Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added unit tests for fetcher, checker and matchers packages #185

Merged
merged 5 commits into from
Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import (
"fmt"

checkererrors "github.com/preslavmihaylov/todocheck/checker/errors"
"github.com/preslavmihaylov/todocheck/fetcher"
"github.com/preslavmihaylov/todocheck/issuetracker/taskstatus"
"github.com/preslavmihaylov/todocheck/matchers"
)

type Fetcher interface {
Fetch(taskID string) (taskstatus.TaskStatus, error)
}

// Checker for todo lines
type Checker struct {
statusFetcher *fetcher.Fetcher
statusFetcher Fetcher
}

// New checker
func New(statusFetcher *fetcher.Fetcher) *Checker {
func New(statusFetcher Fetcher) *Checker {
return &Checker{statusFetcher}
}

Expand Down
98 changes: 98 additions & 0 deletions checker/checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package checker

import (
"errors"
"reflect"
"testing"

checkerrors "github.com/preslavmihaylov/todocheck/checker/errors"
"github.com/preslavmihaylov/todocheck/issuetracker/taskstatus"
)

func TestCheck(t *testing.T) {
fetcher := mockFetcher{}
checker := New(&fetcher)
matcher := mockMatcher{}

testLines := []string{}
testLineCnt := 0

testData := []struct {
comment, filename string
todoErr *checkerrors.TODO
err error
}{
{"NotMatch", "", nil, nil},
{"NotValid", "test.go", checkerrors.MalformedTODOErr("test.go", testLines, testLineCnt), nil},
{"FailedFetch", "", nil, errors.New("")},
{"ClosedIssue", "test.go", checkerrors.IssueClosedErr("test.go", testLines, testLineCnt, "ClosedIssue"), nil},
{"NonExistentIssue", "test.go", checkerrors.IssueNonExistentErr("test.go", testLines, testLineCnt, "NonExistentIssue"), nil},
{"Valid", "", nil, nil},
}
for _, tt := range testData {
t.Run(tt.comment, func(t *testing.T) {

todoErr, err := checker.Check(matcher, tt.comment, tt.filename, testLines, testLineCnt)
if !reflect.DeepEqual(todoErr, tt.todoErr) {
t.Errorf("Expected toddErr to be %v, got %v", tt.todoErr, todoErr)
}
if (err == nil) != (tt.err == nil) { // Don't care about the error string
t.Errorf("Expected err to be %v, got %v", tt.err, err)
}
})
}

t.Run("NilMatcher", func(t *testing.T) {
todoErr, err := checker.Check(nil, "", "", testLines, testLineCnt)
if todoErr != nil {
t.Errorf("Expected toddErr to be nil, got %v", todoErr)
}
if err == nil { // Don't care about the error string
t.Errorf("Expected err to be not nil")
}
})
t.Run("InvalidExtract", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Logf("Recovered in %v", r)
}
}()
_, err := checker.Check(matcher, "InvalidExtract", "", testLines, testLineCnt)
if err != nil {
t.Errorf("Expected err to be nil, got %v", err)
}
t.Errorf("Expected code to panic")
})
}

type mockMatcher struct {
}

func (m mockMatcher) IsMatch(expr string) bool {
return expr != "NotMatch"
}
func (m mockMatcher) IsValid(expr string) bool {
return expr != "NotValid"
}
func (m mockMatcher) ExtractIssueRef(expr string) (string, error) {
if expr == "InvalidExtract" {
return expr, errors.New("Invalid todo")
}
return expr, nil
}

type mockFetcher struct {
}

func (f *mockFetcher) Fetch(taskID string) (taskstatus.TaskStatus, error) {
if taskID == "FailedFetch" {
return 0, errors.New("FailedFetch")
}
if taskID == "ClosedIssue" {
return 2, nil
}
if taskID == "NonExistentIssue" {
return 3, nil
}
return 0, nil
}
7 changes: 4 additions & 3 deletions fetcher/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import (
// Fetcher for task statuses by contacting task management web apps' rest api
type Fetcher struct {
issuetracker.IssueTracker
sendRequest func(req *http.Request) (*http.Response, error)
}

// NewFetcher instance
func NewFetcher(issueTracker issuetracker.IssueTracker) *Fetcher {
return &Fetcher{issueTracker}
httpClient := &http.Client{}
return &Fetcher{issueTracker, httpClient.Do}
}

// Fetch a task's status based on task ID
Expand All @@ -32,8 +34,7 @@ func (f *Fetcher) Fetch(taskID string) (taskstatus.TaskStatus, error) {
return taskstatus.None, fmt.Errorf("couldn't instrument authentication middleware: %w", err)
}

hclient := &http.Client{}
resp, err := hclient.Do(req)
resp, err := f.sendRequest(req)
if err != nil {
return taskstatus.None, fmt.Errorf("couldn't execute GET request: %w", err)
}
Expand Down
155 changes: 155 additions & 0 deletions fetcher/fetcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package fetcher

import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"testing"

"github.com/preslavmihaylov/todocheck/issuetracker"
"github.com/preslavmihaylov/todocheck/issuetracker/taskstatus"
)

var errTest = errors.New("")

func TestFetch(t *testing.T) {
fetcher := NewFetcher(mockIssueTracker{})
testJSON, err := json.Marshal(mockTask{})
if err != nil {
t.Fatalf("Test json is bad")
}

testData := []struct {
Task string
Client mockClient
Status int
Err error
}{
{
Task: "GoodFetch",
Client: mockClient{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(testJSON)), Err: nil},
Status: 1,
Err: nil,
},
{
Task: "BadURL",
Client: mockClient{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(testJSON)), Err: nil},
Status: 0,
Err: errTest,
},
{
Task: "MiddlewareFailure",
Client: mockClient{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(testJSON)), Err: nil},
Status: 0,
Err: errTest,
},
{
Task: "FailedSendingRequest",
Client: mockClient{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(testJSON)), Err: errTest},
Status: 0,
Err: errTest,
},
{
Task: "BadReader",
Client: mockClient{StatusCode: 200, Body: errReader(0), Err: nil},
Status: 0,
Err: errTest,
},
{
Task: "ResponseStatusNotFound",
Client: mockClient{StatusCode: 404, Body: io.NopCloser(bytes.NewReader(testJSON)), Err: nil},
Status: 3,
Err: nil,
},
{
Task: "ResponseBadStatus",
Client: mockClient{StatusCode: 405, Body: io.NopCloser(bytes.NewReader(testJSON)), Err: nil},
Status: 0,
Err: errTest,
},
{
Task: "BadJson",
Client: mockClient{StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte{})), Err: nil},
Status: 0,
Err: errTest,
},
}
for _, tt := range testData {
t.Run(tt.Task, func(t *testing.T) {
fetcher.sendRequest = tt.Client.sendRequest
taskStatus, err := fetcher.Fetch(tt.Task)
if taskStatus != taskstatus.TaskStatus(tt.Status) {
t.Errorf("Task status is %v, expected %v", taskStatus, taskstatus.TaskStatus(tt.Status))
}
if (err == nil) != (tt.Err == nil) { // Doesn't care about error message or type
t.Errorf("Fetch error is %v, expected %v", err, tt.Err)
}
})
}

}

// Mocking Task
type mockTask struct {
Status string
}

func (t mockTask) GetStatus() (taskstatus.TaskStatus, error) {
return taskstatus.Open, nil
}

// Mocking IssueTracker
type mockIssueTracker struct {
}

func (it mockIssueTracker) TaskModel() issuetracker.Task {
return &mockTask{}
}

func (it mockIssueTracker) IssueURLFor(taskID string) string {
if taskID == "BadURL" {
return string(byte(' ') - 1) // This causes http.NewRequest to fail
}
return taskID
}

func (it mockIssueTracker) Exists() bool { // Never called
return false
}

func (it mockIssueTracker) InstrumentMiddleware(r *http.Request) error {
if r.URL.Path == "MiddlewareFailure" { // The taskID is set as URL path, so we're using that as our fail trigger
return errTest
}
return nil
}

func (it mockIssueTracker) TokenAcquisitionInstructions() string { // Never called
return ""
}

// Mocking sendRequest
type mockClient struct {
StatusCode int
Body io.ReadCloser
Err error
}

func (c mockClient) sendRequest(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: c.StatusCode,
Body: c.Body,
}, c.Err
}

type errReader int

func (r errReader) Read(body []byte) (int, error) {
return 0, errTest
}

func (r errReader) Close() error {
return nil
}
64 changes: 64 additions & 0 deletions matchers/matchers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package matchers

import (
"reflect"
"testing"

"github.com/preslavmihaylov/todocheck/matchers/state"
)

func TestTodoMatcherForFile(t *testing.T) {
testTodo := []string{"// TODO"}

for extension, factory := range supportedMatchers {
t.Run(extension, func(t *testing.T) {
matcher := TodoMatcherForFile("test"+extension, testTodo)
want := factory.newTodoMatcher(testTodo)
if matcher != want {
t.Errorf("got %v want %v", matcher, want)
}
})
}

t.Run("Unsupported extension", func(t *testing.T) {
matcher := TodoMatcherForFile("test.md", testTodo)
if matcher != nil {
t.Errorf("Expected nil matcher")
}
})
}

func TestCommentMatcherForFile(t *testing.T) {
var testCommentCallback state.CommentCallback

for extension, factory := range supportedMatchers {
t.Run(extension, func(t *testing.T) {
matcher := CommentMatcherForFile("test"+extension, testCommentCallback)
want := factory.newCommentsMatcher(testCommentCallback)
if !reflect.DeepEqual(matcher, want) {
t.Errorf("got %v want %v", matcher, want)
}
})
}

t.Run("Unsupported extension", func(t *testing.T) {
matcher := CommentMatcherForFile("test.md", testCommentCallback)
if matcher != nil {
t.Errorf("Expected nil matcher")
}
})
}

func TestSupportedFileExtensions(t *testing.T) {
extensions := SupportedFileExtensions()

for _, ext := range extensions {
if _, ok := supportedMatchers[ext]; !ok {
t.Errorf("Extension %v is not in supported extensions", ext)
}
}

if len(extensions) != len(supportedMatchers) {
t.Errorf("Some extensions from supported extensions are missing")
}
}