package gitea_test import ( "context" "encoding/base64" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "git.d-ma.be/mathias/gitea-mcp/internal/gitea" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func b64(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) } // codeSearchFake serves GetRepo + GetTree + GetFileContents off an in-memory // file map — the REAL endpoints SearchCode uses now that Gitea's REST API has // no code-content-search endpoint (confirmed against a live 1.25.5 instance's // swagger spec: only /repos/search, /repos/issues/search etc exist — the web // UI's own code search falls back to server-side `git grep`, an HTML-only // route, not JSON API). Records which paths were actually fetched, so tests // can assert a file was SKIPPED (binary ext, oversized) without ever being // read, not just absent from results. type codeSearchFake struct { files map[string]string // path -> content sizes map[string]int64 // path -> tree-entry size (defaults to len(content)) fetched []string } func (f *codeSearchFake) handler(t *testing.T, owner, repo, branch string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") p := r.URL.Path switch { case r.Method == http.MethodGet && p == "/api/v1/repos/"+owner+"/"+repo: _, _ = fmt.Fprintf(w, `{"name":%q,"full_name":"%s/%s","default_branch":%q}`, repo, owner, repo, branch) case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/repos/"+owner+"/"+repo+"/git/trees/"): var entries []string for path, content := range f.files { size := int64(len(content)) if s, ok := f.sizes[path]; ok { size = s } entries = append(entries, fmt.Sprintf(`{"path":%q,"type":"blob","sha":"s","size":%d}`, path, size)) } _, _ = fmt.Fprintf(w, `{"sha":"root","tree":[%s],"truncated":false}`, strings.Join(entries, ",")) case r.Method == http.MethodGet && strings.HasPrefix(p, "/api/v1/repos/"+owner+"/"+repo+"/contents/"): path := strings.TrimPrefix(p, "/api/v1/repos/"+owner+"/"+repo+"/contents/") f.fetched = append(f.fetched, path) content, ok := f.files[path] if !ok { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"message":"not found"}`)) return } _, _ = fmt.Fprintf(w, `{"path":%q,"sha":"s","size":%d,"content":%q,"encoding":"base64"}`, path, len(content), b64(content)) default: t.Errorf("unexpected request: %s %s", r.Method, p) w.WriteHeader(http.StatusNotFound) } } } func TestSearchCode_FindsMatchInTree(t *testing.T) { f := &codeSearchFake{files: map[string]string{ "internal/gitea/code_search.go": "func (c *Client) SearchCode(ctx context.Context) {}\n", "internal/gitea/repos.go": "func (c *Client) ListRepos() {}\n", }} srv := httptest.NewServer(f.handler(t, "mathias", "infra", "main")) defer srv.Close() c := gitea.NewClient(srv.URL, "tok") hits, err := c.SearchCode(context.Background(), "mathias", "infra", "SearchCode", 1, 30) require.NoError(t, err) require.Len(t, hits, 1) assert.Equal(t, "internal/gitea/code_search.go", hits[0].Path) assert.Contains(t, hits[0].Snippet, "SearchCode") assert.Contains(t, hits[0].HTMLURL, "internal/gitea/code_search.go") assert.Equal(t, 1.0, hits[0].Score) } func TestSearchCode_CaseInsensitive(t *testing.T) { f := &codeSearchFake{files: map[string]string{ "README.md": "# MyProject\n\nBuild instructions here.\n", }} srv := httptest.NewServer(f.handler(t, "mathias", "infra", "main")) defer srv.Close() c := gitea.NewClient(srv.URL, "tok") hits, err := c.SearchCode(context.Background(), "mathias", "infra", "myproject", 1, 30) require.NoError(t, err) require.Len(t, hits, 1) } func TestSearchCode_SkipsBinaryExtensionWithoutFetching(t *testing.T) { f := &codeSearchFake{files: map[string]string{ "assets/logo.png": "SearchCode", // would match if fetched — must not be "main.go": "package main\n", }} srv := httptest.NewServer(f.handler(t, "mathias", "infra", "main")) defer srv.Close() c := gitea.NewClient(srv.URL, "tok") hits, err := c.SearchCode(context.Background(), "mathias", "infra", "SearchCode", 1, 30) require.NoError(t, err) assert.Empty(t, hits) assert.NotContains(t, f.fetched, "assets/logo.png", "binary extension must be skipped before fetch") } func TestSearchCode_SkipsOversizedFileWithoutFetching(t *testing.T) { f := &codeSearchFake{ files: map[string]string{"vendor/bundle.txt": "SearchCode"}, sizes: map[string]int64{"vendor/bundle.txt": 10 * 1024 * 1024}, // 10MB per the tree listing } srv := httptest.NewServer(f.handler(t, "mathias", "infra", "main")) defer srv.Close() c := gitea.NewClient(srv.URL, "tok") hits, err := c.SearchCode(context.Background(), "mathias", "infra", "SearchCode", 1, 30) require.NoError(t, err) assert.Empty(t, hits) assert.NotContains(t, f.fetched, "vendor/bundle.txt", "oversized file must be skipped before fetch") } func TestSearchCode_SkipsBinaryContentNullByte(t *testing.T) { f := &codeSearchFake{files: map[string]string{ "data.bin": "SearchCode\x00binary-marker", }} srv := httptest.NewServer(f.handler(t, "mathias", "infra", "main")) defer srv.Close() c := gitea.NewClient(srv.URL, "tok") hits, err := c.SearchCode(context.Background(), "mathias", "infra", "SearchCode", 1, 30) require.NoError(t, err) assert.Empty(t, hits, "content with a null byte must be treated as binary even past the extension filter") } func TestSearchCode_Pagination(t *testing.T) { files := map[string]string{} for i := 0; i < 5; i++ { files[fmt.Sprintf("file%d.go", i)] = strings.Repeat("hit ", i+1) // increasing occurrence count => increasing score } f := &codeSearchFake{files: files} srv := httptest.NewServer(f.handler(t, "mathias", "infra", "main")) defer srv.Close() c := gitea.NewClient(srv.URL, "tok") page1, err := c.SearchCode(context.Background(), "mathias", "infra", "hit", 1, 2) require.NoError(t, err) require.Len(t, page1, 2) page2, err := c.SearchCode(context.Background(), "mathias", "infra", "hit", 2, 2) require.NoError(t, err) require.Len(t, page2, 2) assert.NotEqual(t, page1[0].Path, page2[0].Path, "pages must not overlap") assert.True(t, page1[0].Score >= page1[1].Score, "page1 sorted desc by score") assert.True(t, page1[1].Score >= page2[0].Score, "page1's worst must rank >= page2's best") } func TestSearchCode_EmptyQueryErrors(t *testing.T) { c := gitea.NewClient("http://unused", "tok") _, err := c.SearchCode(context.Background(), "mathias", "infra", "", 1, 30) require.Error(t, err) assert.True(t, errors.Is(err, gitea.ErrValidation)) }