Client GitHub avec cache, quota et tests ; README

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-01 12:47:35 +02:00
1 parent 1885488d05
commit 9432a34e30
3 files changed
+416 -2

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
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)
}
}
}