package gitcode
import (
"context"
"encoding/json"
"errors"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
type fixtureProvider struct {
cfg FixtureConfig
repo Repo
issues []IssueSummary
issue map[int]Issue
comments map[int][]Comment
wikis []WikiPage
wiki map[string]WikiPage
search []SearchResult
}
func NewFixtureProvider(cfg FixtureConfig) (Provider, error) {
if cfg.Owner == "" {
cfg.Owner = "example-owner"
}
if cfg.Repo == "" {
cfg.Repo = "example-repo"
}
if cfg.RootDir == "" {
cfg.RootDir = filepath.Join("..", "..", "fixtures")
}
if cfg.BaseURL == "" {
cfg.BaseURL = "https://api.example.com"
}
p := &fixtureProvider{cfg: cfg, issue: map[int]Issue{}, comments: map[int][]Comment{}, wiki: map[string]WikiPage{}}
p.repo = Repo{ID: "REPO-EXAMPLE", Owner: cfg.Owner, Name: cfg.Repo, FullName: cfg.Owner + "/" + cfg.Repo, DefaultRef: "main", Description: "sanitized fixture repository", HTMLURL: cfg.BaseURL + "/" + cfg.Owner + "/" + cfg.Repo}
if err := p.load(); err != nil {
return nil, err
}
return p, nil
}
func (p *fixtureProvider) load() error {
base := filepath.Join(p.cfg.RootDir, "api", "v5", "repos", p.cfg.Owner, p.cfg.Repo)
if err := readFixture(filepath.Join(base, "issues.json"), &p.issues); err != nil {
return err
}
for _, item := range p.issues {
var issue Issue
if err := readFixture(filepath.Join(base, "issues", strconv.Itoa(item.Number)+".json"), &issue); err == nil {
p.issue[issue.Number] = issue
} else if errors.Is(err, os.ErrNotExist) {
p.issue[item.Number] = Issue{ID: item.ID, Number: item.Number, Title: item.Title, Status: item.Status, State: item.State, Labels: append([]string(nil), item.Labels...), CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt}
} else {
return err
}
var comments []Comment
if err := readFixture(filepath.Join(base, "issues", strconv.Itoa(item.Number), "comments.json"), &comments); err == nil {
p.comments[item.Number] = comments
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
}
if err := readFixture(filepath.Join(base, "wiki", "pages.json"), &p.wikis); err != nil {
return err
}
for _, page := range p.wikis {
var full WikiPage
if err := readFixture(filepath.Join(base, "wiki", page.Slug+".json"), &full); err == nil {
p.wiki[full.Slug] = full
if full.ID != "" {
p.wiki[full.ID] = full
}
} else if errors.Is(err, os.ErrNotExist) {
p.wiki[page.Slug] = page
if page.ID != "" {
p.wiki[page.ID] = page
}
} else {
return err
}
}
for _, issue := range p.issue {
p.search = append(p.search, SearchResult{ID: issue.ID, Type: "issue", Title: issue.Title, Body: issue.Body, URL: p.cfg.BaseURL + "/" + p.cfg.Owner + "/" + p.cfg.Repo + "/issues/" + strconv.Itoa(issue.Number), Score: 1, CreatedAt: issue.CreatedAt, UpdatedAt: issue.UpdatedAt})
}
for _, page := range p.wiki {
p.search = append(p.search, SearchResult{ID: page.ID, Type: "wiki", Title: page.Title, Body: page.Body, URL: p.cfg.BaseURL + "/" + p.cfg.Owner + "/" + p.cfg.Repo + "/wiki/" + page.Slug, Score: 1, CreatedAt: page.CreatedAt, UpdatedAt: page.UpdatedAt})
}
sort.Slice(p.search, func(i, j int) bool { return p.search[i].ID < p.search[j].ID })
return nil
}
func readFixture(path string, out any) error {
body, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(body, out)
}
func (p *fixtureProvider) FixtureBoundaryMode() string {
return FixtureBoundaryMode
}
func (p *fixtureProvider) FixtureMarkerIDs() []string {
return FixtureMarkerIDs()
}
func (p *fixtureProvider) ProbeAuth(context.Context, AuthProbeRequest) (AuthProbeResult, error) {
if err := p.scenarioError("auth"); err != nil {
return AuthProbeResult{}, err
}
return AuthProbeResult{Authenticated: true, TokenPresent: false, Scopes: []string{"issues", "wiki"}, User: "fixture-user", CheckedAt: time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC), Mode: string(ProviderModeFixture)}, nil
}
func (p *fixtureProvider) GetRepo(context.Context, RepoRequest) (Repo, error) {
if err := p.scenarioError("repo"); err != nil {
return Repo{}, err
}
return p.repo, nil
}
func (p *fixtureProvider) ListIssues(ctx context.Context, req IssueListRequest) (Page[IssueSummary], error) {
if err := p.scenarioError("issues"); err != nil {
return Page[IssueSummary]{}, err
}
items := filterIssues(p.issues, req)
return paginateFixture(ctx, listIssuesEndpoint(req.Owner, req.Repo), items, PageState{Page: req.Page, PerPage: req.PerPage}, p.cfg.Pagination, p.cfg.Scenario)
}
func (p *fixtureProvider) GetIssue(_ context.Context, req IssueRequest) (Issue, error) {
if err := p.scenarioError("issue"); err != nil {
return Issue{}, err
}
issue, ok := p.issue[req.Number]
if !ok {
return Issue{}, ErrNotFound{Endpoint: getIssueEndpoint(req.Owner, req.Repo, req.Number), ID: strconv.Itoa(req.Number)}
}
return issue, nil
}
func (p *fixtureProvider) ListIssueComments(ctx context.Context, req IssueRequest) (Page[Comment], error) {
if err := p.scenarioError("comments"); err != nil {
return Page[Comment]{}, err
}
return paginateFixture(ctx, listIssueCommentsEndpoint(req.Owner, req.Repo, req.Number), append([]Comment(nil), p.comments[req.Number]...), PageState{}, p.cfg.Pagination, p.cfg.Scenario)
}
func (p *fixtureProvider) ListPRs(context.Context, PRListRequest) (Page[PullRequest], error) {
return Page[PullRequest]{}, FixtureReadOnlyError("ListPRs")
}
func (p *fixtureProvider) GetPR(context.Context, PRRequest) (PullRequest, error) {
return PullRequest{}, FixtureReadOnlyError("GetPR")
}
func (p *fixtureProvider) ListPRComments(context.Context, PRRequest) (Page[PRComment], error) {
return Page[PRComment]{}, FixtureReadOnlyError("ListPRComments")
}
func (p *fixtureProvider) CreatePR(context.Context, CreatePRRequest, WriteOptions) (WriteResult[PullRequest], error) {
return WriteResult[PullRequest]{}, FixtureReadOnlyError("CreatePR")
}
func (p *fixtureProvider) UpdatePR(context.Context, UpdatePRRequest, WriteOptions) (WriteResult[PullRequest], error) {
return WriteResult[PullRequest]{}, FixtureReadOnlyError("UpdatePR")
}
func (p *fixtureProvider) MergePR(context.Context, MergePRRequest, WriteOptions) (WriteResult[PullRequest], error) {
return WriteResult[PullRequest]{}, FixtureReadOnlyError("MergePR")
}
func (p *fixtureProvider) LinkPRIssue(context.Context, LinkPRIssueRequest, WriteOptions) (WriteResult[[]Issue], error) {
return WriteResult[[]Issue]{}, FixtureReadOnlyError("LinkPRIssue")
}
func (p *fixtureProvider) ListWikiPages(ctx context.Context, req WikiListRequest) (Page[WikiPage], error) {
if err := p.scenarioError("wiki"); err != nil {
return Page[WikiPage]{}, err
}
return paginateFixture(ctx, listWikiPagesEndpoint(req.Owner, req.Repo), append([]WikiPage(nil), p.wikis...), PageState{Page: req.Page, PerPage: req.PerPage}, p.cfg.Pagination, p.cfg.Scenario)
}
func (p *fixtureProvider) GetWikiPage(_ context.Context, req WikiPageRequest) (WikiPage, error) {
if err := p.scenarioError("wiki-page"); err != nil {
return WikiPage{}, err
}
wikiPath := wikiRequestPath(req.Path, req.Slug)
page, ok := p.wiki[wikiPath]
if !ok {
return WikiPage{}, ErrNotFound{Endpoint: getWikiPageEndpoint(req.Owner, req.Repo, wikiPath), ID: wikiPath}
}
return page, nil
}
func (p *fixtureProvider) Search(ctx context.Context, req SearchRequest) (Page[SearchResult], error) {
if err := p.scenarioError("search"); err != nil {
return Page[SearchResult]{}, err
}
items := make([]SearchResult, 0, len(p.search))
query := strings.ToLower(strings.TrimSpace(req.Query))
for _, item := range p.search {
if req.Type != "" && item.Type != req.Type {
continue
}
if query == "" || strings.Contains(strings.ToLower(item.Title), query) || strings.Contains(strings.ToLower(item.Body), query) {
items = append(items, item)
}
}
return paginateFixture(ctx, searchIssuesEndpoint(), items, PageState{Page: req.Page, PerPage: req.PerPage}, p.cfg.Pagination, p.cfg.Scenario)
}
func (p *fixtureProvider) CreateIssue(context.Context, CreateIssueRequest, WriteOptions) (WriteResult[Issue], error) {
return WriteResult[Issue]{}, FixtureReadOnlyError("CreateIssue")
}
func (p *fixtureProvider) UpdateIssue(context.Context, UpdateIssueRequest, WriteOptions) (WriteResult[Issue], error) {
return WriteResult[Issue]{}, FixtureReadOnlyError("UpdateIssue")
}
func (p *fixtureProvider) CreateIssueComment(context.Context, CreateIssueCommentRequest, WriteOptions) (WriteResult[Comment], error) {
return WriteResult[Comment]{}, FixtureReadOnlyError("CreateIssueComment")
}
func (p *fixtureProvider) UpdateIssueComment(context.Context, UpdateIssueCommentRequest, WriteOptions) (WriteResult[Comment], error) {
return WriteResult[Comment]{}, FixtureReadOnlyError("UpdateIssueComment")
}
func (p *fixtureProvider) CreatePRComment(context.Context, CreatePRCommentRequest, WriteOptions) (WriteResult[PRComment], error) {
return WriteResult[PRComment]{}, FixtureReadOnlyError("CreatePRComment")
}
func (p *fixtureProvider) CreatePRReviewComment(context.Context, CreatePRReviewCommentRequest, WriteOptions) (WriteResult[PRComment], error) {
return WriteResult[PRComment]{}, FixtureReadOnlyError("CreatePRReviewComment")
}
func (p *fixtureProvider) ReplyPRReviewComment(context.Context, ReplyPRReviewCommentRequest, WriteOptions) (WriteResult[PRComment], error) {
return WriteResult[PRComment]{}, FixtureReadOnlyError("ReplyPRReviewComment")
}
func (p *fixtureProvider) CreateWikiPage(context.Context, CreateWikiPageRequest, WriteOptions) (WriteResult[WikiPage], error) {
return WriteResult[WikiPage]{}, FixtureReadOnlyError("CreateWikiPage")
}
func (p *fixtureProvider) UpdateWikiPage(context.Context, UpdateWikiPageRequest, WriteOptions) (WriteResult[WikiPage], error) {
return WriteResult[WikiPage]{}, FixtureReadOnlyError("UpdateWikiPage")
}
func (p *fixtureProvider) DeleteWikiPage(context.Context, DeleteWikiPageRequest, WriteOptions) (WriteResult[WikiPage], error) {
return WriteResult[WikiPage]{}, FixtureReadOnlyError("DeleteWikiPage")
}
func (p *fixtureProvider) ListMilestones(context.Context, MilestoneListRequest) (Page[Milestone], error) {
return Page[Milestone]{}, FixtureReadOnlyError("ListMilestones")
}
func (p *fixtureProvider) ListPushRemoteMirrors(context.Context, PushMirrorListRequest) ([]PushMirror, error) {
return nil, FixtureReadOnlyError("ListPushRemoteMirrors")
}
func (p *fixtureProvider) TriggerPushRemoteMirror(context.Context, PushMirrorTriggerRequest, WriteOptions) (WriteResult[PushMirror], error) {
return WriteResult[PushMirror]{}, FixtureReadOnlyError("TriggerPushRemoteMirror")
}
func (p *fixtureProvider) GetMilestone(context.Context, MilestoneRequest) (Milestone, error) {
return Milestone{}, FixtureReadOnlyError("GetMilestone")
}
func (p *fixtureProvider) CreateMilestone(context.Context, MilestoneWriteRequest, WriteOptions) (WriteResult[Milestone], error) {
return WriteResult[Milestone]{}, FixtureReadOnlyError("CreateMilestone")
}
func (p *fixtureProvider) UpdateMilestone(context.Context, MilestoneWriteRequest, WriteOptions) (WriteResult[Milestone], error) {
return WriteResult[Milestone]{}, FixtureReadOnlyError("UpdateMilestone")
}
func (p *fixtureProvider) GetRelease(context.Context, ReleaseRequest) (Release, error) {
return Release{}, FixtureReadOnlyError("GetRelease")
}
func (p *fixtureProvider) CreateRelease(context.Context, ReleaseWriteRequest, WriteOptions) (WriteResult[Release], error) {
return WriteResult[Release]{}, FixtureReadOnlyError("CreateRelease")
}
func (p *fixtureProvider) UpdateRelease(context.Context, ReleaseWriteRequest, WriteOptions) (WriteResult[Release], error) {
return WriteResult[Release]{}, FixtureReadOnlyError("UpdateRelease")
}
func (p *fixtureProvider) scenarioError(endpoint string) error {
switch p.cfg.Scenario {
case "auth-error":
return ErrAuthExpired{Endpoint: endpoint, Status: http.StatusUnauthorized, Message: "fixture auth expired"}
case "conflict":
return ErrConflict{Endpoint: endpoint, Status: http.StatusConflict, RemotePayload: []byte(`{"message":"fixture conflict"}`), Message: "fixture conflict"}
case "rate-limit":
return ErrRateLimited{Endpoint: endpoint, Attempts: 1, RetryAfter: time.Second, RawRetryAfter: "1"}
default:
return nil
}
}
func filterIssues(items []IssueSummary, req IssueListRequest) []IssueSummary {
out := make([]IssueSummary, 0, len(items))
for _, item := range items {
if req.State != "" && req.State != "all" && item.State != req.State && item.Status != req.State {
continue
}
if len(req.Labels) > 0 && !hasAllLabels(item.Labels, req.Labels) {
continue
}
out = append(out, item)
}
return out
}
func hasAllLabels(have, want []string) bool {
seen := map[string]bool{}
for _, label := range have {
seen[label] = true
}
for _, label := range want {
if !seen[label] {
return false
}
}
return true
}