func TestError(t *testing.T) { err := gddoexp.NewError("path/to/project", gddoexp.ErrorCodeRetrieveImportCounts, fmt.Errorf("i'm a crazy error")) expected := "gddoexp: [path/to/project] error retrieving import counts: i'm a crazy error" if msg := err.Error(); msg != expected { t.Errorf("expected “%s” and got “%s”", expected, msg) } err = gddoexp.NewError("path/to/project", gddoexp.ErrorCodeNonGithub, nil) expected = "gddoexp: [path/to/project] not a Github project" if msg := err.Error(); msg != expected { t.Errorf("expected “%s” and got “%s”", expected, msg) } }
func TestIsFastForkPackage(t *testing.T) { data := []struct { description string path string auth *gddoexp.GithubAuth httpClient httpClientMock expected bool expectedCache bool expectedError error }{ { description: "it should detect a fast fork package (without authentication)", path: "github.com/rafaeljusto/dns", httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url == "https://api.github.com/repos/rafaeljusto/dns" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "fork": true }`)), }, nil } else if url == "https://api.github.com/repos/rafaeljusto/dns/commits" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`[ { "commit": { "author": { "date": "2010-08-03T23:00:00Z" } } } ]`)), }, nil } else { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } }, }, expected: true, }, { description: "it should detect a fast fork package (authenticated)", path: "github.com/rafaeljusto/dns", auth: &gddoexp.GithubAuth{ ID: "exampleuser", Secret: "abc123", }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url == "https://api.github.com/repos/rafaeljusto/dns?client_id=exampleuser&client_secret=abc123" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "fork": true }`)), }, nil } else if url == "https://api.github.com/repos/rafaeljusto/dns/commits?client_id=exampleuser&client_secret=abc123" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`[ { "commit": { "author": { "date": "2010-08-03T23:00:00Z" } } } ]`)), }, nil } else { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } }, }, expected: true, }, { description: "it should fail when there's a HTTP problem with Github API (repo request)", path: "github.com/rafaeljusto/gddoexp", httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return nil, fmt.Errorf("i'm a crazy error") }, }, expectedError: gddoexp.NewError("github.com/rafaeljusto/gddoexp", gddoexp.ErrorCodeGithubFetch, fmt.Errorf("i'm a crazy error")), }, { description: "it should detect that is not a fast fork when the repository isn't a fork", path: "github.com/miekg/dns", httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url == "https://api.github.com/repos/miekg/dns" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "fork": false }`)), }, nil } return &http.Response{ StatusCode: http.StatusBadRequest, }, nil }, }, expected: false, }, { description: "it should fail when there's a HTTP problem with Github API (commits request)", path: "github.com/rafaeljusto/dns", httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url == "https://api.github.com/repos/rafaeljusto/dns" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "fork": true }`)), }, nil } return nil, fmt.Errorf("i'm a crazy error") }, }, expectedError: gddoexp.NewError("github.com/rafaeljusto/dns", gddoexp.ErrorCodeGithubFetch, fmt.Errorf("i'm a crazy error")), }, { description: "it should detect that is not a fast fork when there's a commit after the commits period", path: "github.com/rafaeljusto/dns", httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url == "https://api.github.com/repos/rafaeljusto/dns" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "fork": true }`)), }, nil } else if url == "https://api.github.com/repos/rafaeljusto/dns/commits" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`[ { "commit": { "author": { "date": "2010-08-03T23:00:00Z" } } }, { "commit": { "author": { "date": "2010-08-11T21:56:24Z" } } } ]`)), }, nil } else { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } }, }, expected: false, }, { description: "it should detect that is not a fast fork when there are too many commits ", path: "github.com/rafaeljusto/dns", httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url == "https://api.github.com/repos/rafaeljusto/dns" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "fork": true }`)), }, nil } else if url == "https://api.github.com/repos/rafaeljusto/dns/commits" { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`[ { "commit": { "author": { "date": "2010-08-03T23:00:00Z" } } }, { "commit": { "author": { "date": "2010-08-03T23:01:00Z" } } }, { "commit": { "author": { "date": "2010-08-03T23:02:00Z" } } } ]`)), }, nil } else { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } }, }, expected: false, }, { description: "it should fail when it's not a Github project", path: "bitbucket.org/rafaeljusto/gddoexp", expectedCache: true, expectedError: gddoexp.NewError("bitbucket.org/rafaeljusto/gddoexp", gddoexp.ErrorCodeNonGithub, nil), }, } httpClientBkp := gddoexp.HTTPClient defer func() { gddoexp.HTTPClient = httpClientBkp }() isCacheResponseBkp := gddoexp.IsCacheResponse defer func() { gddoexp.IsCacheResponse = isCacheResponseBkp }() gddoexp.IsCacheResponse = func(r *http.Response) bool { return r.Header.Get("Cache") == "1" } for i, item := range data { gddoexp.HTTPClient = item.httpClient p := database.Package{ Path: item.path, } fastFork, cache, err := gddoexp.IsFastForkPackage(p, item.auth) if fastFork != item.expected { if item.expected { t.Errorf("[%d] %s: expected package to be a fast fork", i, item.description) } else { t.Errorf("[%d] %s: expected package to don't be a fast fork", i, item.description) } } if cache != item.expectedCache { if item.expectedCache { t.Errorf("[%d] %s: expected hit in cache", i, item.description) } else { t.Errorf("[%d] %s: unexpected hit in cache", i, item.description) } } if !reflect.DeepEqual(item.expectedError, err) { t.Errorf("[%d] %s: expected error to be “%v” and got “%v”", i, item.description, item.expectedError, err) } } }
func TestShouldSuppressPackage(t *testing.T) { data := []struct { description string path string db databaseMock auth *gddoexp.GithubAuth httpClient httpClientMock expected bool expectedCache bool expectedError error }{ { description: "it should suppress a package (without authentication)", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url != "https://api.github.com/repos/rafaeljusto/gddoexp" { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "forks_count": 194, "network_count": 194, "stargazers_count": 1133, "updated_at": "` + time.Now().Add(-2*365*24*time.Hour).Format(time.RFC3339) + `" }`)), }, nil }, }, expected: true, }, { description: "it should suppress a package from cache (without authentication)", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url != "https://api.github.com/repos/rafaeljusto/gddoexp" { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } return &http.Response{ StatusCode: http.StatusOK, Header: http.Header{ "Cache": []string{"1"}, }, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "forks_count": 194, "network_count": 194, "stargazers_count": 1133, "updated_at": "` + time.Now().Add(-2*365*24*time.Hour).Format(time.RFC3339) + `" }`)), }, nil }, }, expected: true, expectedCache: true, }, { description: "it should suppress a package (authenticated)", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, auth: &gddoexp.GithubAuth{ ID: "exampleuser", Secret: "abc123", }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url != "https://api.github.com/repos/rafaeljusto/gddoexp?client_id=exampleuser&client_secret=abc123" { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "forks_count": 194, "network_count": 194, "stargazers_count": 1133, "updated_at": "` + time.Now().Add(-2*365*24*time.Hour).Format(time.RFC3339) + `" }`)), }, nil }, }, expected: true, }, { description: "it shouldn't suppress a package because of recent commit", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "forks_count": 194, "network_count": 194, "stargazers_count": 1133, "updated_at": "` + time.Now().Add(-2*364*24*time.Hour).Format(time.RFC3339) + `" }`)), }, nil }, }, expected: false, }, { description: "it shouldn't suppress a package because of import reference", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 1, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "forks_count": 194, "network_count": 194, "stargazers_count": 1133, "updated_at": "` + time.Now().Add(-2*365*24*time.Hour).Format(time.RFC3339) + `" }`)), }, nil }, }, expected: false, expectedCache: true, }, { description: "it should suppress a package (project subpath)", path: "github.com/rafaeljusto/gddoexp/cmd/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { if url != "https://api.github.com/repos/rafaeljusto/gddoexp" { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "forks_count": 194, "network_count": 194, "stargazers_count": 1133, "updated_at": "` + time.Now().Add(-2*365*24*time.Hour).Format(time.RFC3339) + `" }`)), }, nil }, }, expected: true, }, { description: "it should fail to retrive the import counts from GoDoc database", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, fmt.Errorf("i'm a crazy error") }, }, expectedCache: true, expectedError: gddoexp.NewError("github.com/rafaeljusto/gddoexp", gddoexp.ErrorCodeRetrieveImportCounts, fmt.Errorf("i'm a crazy error")), }, { description: "it should fail when it's not a Github project", path: "bitbucket.org/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, expectedCache: true, expectedError: gddoexp.NewError("bitbucket.org/rafaeljusto/gddoexp", gddoexp.ErrorCodeNonGithub, nil), }, { description: "it should fail when there's a HTTP problem with Github API", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return nil, fmt.Errorf("i'm a crazy error") }, }, expectedError: gddoexp.NewError("github.com/rafaeljusto/gddoexp", gddoexp.ErrorCodeGithubFetch, fmt.Errorf("i'm a crazy error")), }, { description: "it should fail when the HTTP status code from Github API is 403 Forbidden (ratelimit) without reset HTTP header", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusForbidden, }, nil }, }, expectedError: gddoexp.NewError("github.com/rafaeljusto/gddoexp", gddoexp.ErrorCodeGithubForbidden, nil), }, { description: "it should fail when the HTTP status code from Github API is 403 Forbidden (ratelimit) with reset HTTP header", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func() func(string) (*http.Response, error) { var requestNumber int return func(url string) (*http.Response, error) { requestNumber++ if url != "https://api.github.com/repos/rafaeljusto/gddoexp" { return &http.Response{ StatusCode: http.StatusBadRequest, }, nil } switch requestNumber { case 1: return &http.Response{ StatusCode: http.StatusForbidden, Header: http.Header{ "X-Ratelimit-Reset": []string{strconv.FormatInt(time.Now().Add(10*time.Millisecond).Unix(), 10)}, }, }, nil case 2: return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{ "created_at": "2010-08-03T21:56:23Z", "forks_count": 194, "network_count": 194, "stargazers_count": 1133, "updated_at": "` + time.Now().Add(-2*365*24*time.Hour).Format(time.RFC3339) + `" }`)), }, nil } return &http.Response{ StatusCode: http.StatusInternalServerError, }, nil } }(), }, expected: true, }, { description: "it should fail when the HTTP status code from Github API is 404 Not Found", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusNotFound, }, nil }, }, expectedError: gddoexp.NewError("github.com/rafaeljusto/gddoexp", gddoexp.ErrorCodeGithubNotFound, nil), }, { description: "it should fail when the HTTP status code from Github API isn't valid", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusTeapot, }, nil }, }, expectedError: gddoexp.NewError("github.com/rafaeljusto/gddoexp", gddoexp.ErrorCodeGithubStatusCode, nil), }, { description: "it should fail to decode the JSON response from Github", path: "github.com/rafaeljusto/gddoexp", db: databaseMock{ importerCountMock: func(path string) (int, error) { return 0, nil }, }, httpClient: httpClientMock{ getMock: func(url string) (*http.Response, error) { return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(bytes.NewBufferString(`{`)), }, nil }, }, expectedError: gddoexp.NewError("github.com/rafaeljusto/gddoexp", gddoexp.ErrorCodeGithubParse, fmt.Errorf("unexpected EOF")), }, } httpClientBkp := gddoexp.HTTPClient defer func() { gddoexp.HTTPClient = httpClientBkp }() isCacheResponseBkp := gddoexp.IsCacheResponse defer func() { gddoexp.IsCacheResponse = isCacheResponseBkp }() gddoexp.IsCacheResponse = func(r *http.Response) bool { return r.Header.Get("Cache") == "1" } for i, item := range data { gddoexp.HTTPClient = item.httpClient p := database.Package{ Path: item.path, } suppress, cache, err := gddoexp.ShouldSuppressPackage(p, item.db, item.auth) if suppress != item.expected { if item.expected { t.Errorf("[%d] %s: expected package to be suppressed", i, item.description) } else { t.Errorf("[%d] %s: expected package to don't be suppressed", i, item.description) } } if cache != item.expectedCache { if item.expectedCache { t.Errorf("[%d] %s: expected hit in cache", i, item.description) } else { t.Errorf("[%d] %s: unexpected hit in cache", i, item.description) } } if !reflect.DeepEqual(item.expectedError, err) { t.Errorf("[%d] %s: expected error to be “%v” and got “%v”", i, item.description, item.expectedError, err) } } }