package main import ( "context" "io" "net/http" "strings" "testing" "time" ) // fakeGitHub answers every request with a fixed status, headers and body. type fakeGitHub struct { status int header http.Header body string calls int last *http.Request } func (f *fakeGitHub) RoundTrip(r *http.Request) (*http.Response, error) { f.calls++ f.last = r return &http.Response{ StatusCode: f.status, Status: http.StatusText(f.status), Header: f.header, Body: io.NopCloser(strings.NewReader(f.body)), Request: r, }, nil } func newFake(status int, body string) (*GitHub, *fakeGitHub) { f := &fakeGitHub{status: status, body: body, header: http.Header{ "X-Ratelimit-Limit": {"10"}, "X-Ratelimit-Remaining": {"9"}, "X-Ratelimit-Reset": {"1790851447"}, "X-Ratelimit-Resource": {"search"}, }} gh := NewGitHub("https://api.github.test", "secret", time.Minute) gh.client = &http.Client{Transport: f} return gh, f } const searchBody = `{"total_count": 14, "incomplete_results": false, "items": [{ "full_name": "mcuadros/go-syslog", "name": "go-syslog", "html_url": "https://github.com/mcuadros/go-syslog", "description": "Syslog server library for go", "language": "Go", "topics": null, "stargazers_count": 540, "forks_count": 150, "open_issues_count": 20, "archived": false, "fork": false, "created_at": "2014-09-01T10:00:00Z", "pushed_at": "2024-03-01T10:00:00Z", "owner": {"login": "mcuadros", "avatar_url": "https://avatars.githubusercontent.com/u/1573114?v=4"}, "license": {"spdx_id": "MIT"}}]}` func TestSearchMapsAndCaches(t *testing.T) { gh, f := newFake(http.StatusOK, searchBody) c := Criteria{Text: "syslog", Language: "Go", Sort: "stars", Order: "desc", Page: 1, PerPage: 30} res, err := gh.Search(context.Background(), c) if err != nil { t.Fatal(err) } if res.Total != 14 || len(res.Items) != 1 { t.Fatalf("got total %d, %d items", res.Total, len(res.Items)) } r := res.Items[0] if r.FullName != "mcuadros/go-syslog" || r.Owner != "mcuadros" || r.License != "MIT" || r.Stars != 540 || r.Topics == nil { t.Errorf("bad mapping: %+v", r) } if res.Rate.Limit != 10 || res.Rate.Remaining != 9 { t.Errorf("bad rate: %+v", res.Rate) } q := f.last.URL.Query() if q.Get("sort") != "stars" || q.Get("q") != "syslog language:Go archived:false" { t.Errorf("bad request: %s", f.last.URL) } if f.last.Header.Get("Authorization") != "Bearer secret" { t.Errorf("token not sent") } res, err = gh.Search(context.Background(), c) if err != nil || !res.Cached || f.calls != 1 { t.Errorf("second identical search should come from the cache (calls=%d, cached=%v, err=%v)", f.calls, res.Cached, err) } } func TestSearchErrors(t *testing.T) { for status, code := range map[int]string{ http.StatusForbidden: "rate_limited", http.StatusUnauthorized: "github_token", http.StatusUnprocessableEntity: "bad_query", } { gh, _ := newFake(status, `{"message": "nope", "errors": [{"message": "detail"}]}`) _, err := gh.Search(context.Background(), Criteria{Page: 1, PerPage: 30}) ce, ok := err.(*codedError) if !ok || ce.code != code || ce.detail != "detail" { t.Errorf("status %d: got %v, want code %s", status, err, code) } } }