package mentions import ( "context" "errors" "fmt" "strings" "sync" "testing" "time" "github.com/kandev/kandev/internal/common/logger" apiv1 "github.com/kandev/kandev/pkg/api/v1" "go.uber.org/zap" "go.uber.org/zap/zaptest/observer" ) type fakeProvider struct { descriptor ProviderDescriptor search func(context.Context, SearchRequest) ([]Candidate, error) authorize func(context.Context, ReferenceAuthorizationRequest) error } type searchOnlyProvider struct { descriptor ProviderDescriptor } func (p searchOnlyProvider) Descriptor() ProviderDescriptor { return p.descriptor } func (p searchOnlyProvider) Search(context.Context, SearchRequest) ([]Candidate, error) { return nil, nil } func (p fakeProvider) Descriptor() ProviderDescriptor { return p.descriptor } func (p fakeProvider) Search(ctx context.Context, request SearchRequest) ([]Candidate, error) { return p.search(ctx, request) } func (p fakeProvider) AuthorizeReference(ctx context.Context, request ReferenceAuthorizationRequest) error { if p.authorize == nil { return nil } return p.authorize(ctx, request) } func TestServiceSearch_RegistryOwnsNamespacedProviderIdentity(t *testing.T) { registry := NewRegistry() err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{ Source: "plugin:acme:incidents", Provider: "plugin:acme:tracker", Kind: "incident", DisplayName: "Acme incidents", KindLabel: "Incident", }, search: func(_ context.Context, request SearchRequest) ([]Candidate, error) { if request.Query != "auth" { t.Fatalf("query = %q, want trimmed query auth", request.Query) } if request.Limit != DefaultLimit { t.Fatalf("limit = %d, want default %d", request.Limit, DefaultLimit) } return []Candidate{{ ID: "incident-7", Key: "INC-7", Title: "Authentication outage", URL: "https://incidents.example.test/7", Scope: "tenant-a", }}, nil }, }) if err != nil { t.Fatalf("register provider: %v", err) } response, err := NewService(registry).Search(context.Background(), SearchRequest{ WorkspaceID: "workspace-1", Query: " auth ", }) if err != nil { t.Fatalf("search: %v", err) } if len(response.Groups) != 1 || len(response.Groups[0].Results) != 1 { t.Fatalf("groups = %#v, want one group with one result", response.Groups) } group := response.Groups[0] result := group.Results[0] if group.Source != "plugin:acme:incidents" || result.Provider != "plugin:acme:tracker" || result.Kind != "incident" { t.Fatalf("registry identity = source %q provider %q kind %q", group.Source, result.Provider, result.Kind) } const wantRef = "mention:v1:plugin%3Aacme%3Atracker:incident:tenant-a:incident-7" if result.Ref != wantRef { t.Fatalf("ref = %q, want %q", result.Ref, wantRef) } } func TestServiceSearchLogsProviderFailureWithSafeIdentity(t *testing.T) { core, observed := observer.New(zap.DebugLevel) log, err := logger.NewFromZap(zap.New(core)) if err != nil { t.Fatalf("create observer logger: %v", err) } registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{ Source: "acme_issues", Provider: "acme", Kind: "issue", DisplayName: "Acme issues", KindLabel: "Issue", }, search: func(context.Context, SearchRequest) ([]Candidate, error) { return nil, NewProviderError(StatusUnauthorized, errors.New("secret upstream detail")) }, }); err != nil { t.Fatalf("register provider: %v", err) } response, err := NewService(registry, WithLogger(log)).Search(context.Background(), SearchRequest{ WorkspaceID: "workspace-1", Query: "secret-query", }) if err != nil { t.Fatalf("search: %v", err) } if response.Groups[0].Status != StatusUnauthorized { t.Fatalf("provider status = %q, want %q", response.Groups[0].Status, StatusUnauthorized) } entries := observed.All() if len(entries) != 1 || entries[0].Level != zap.WarnLevel { t.Fatalf("provider diagnostics = %#v", entries) } fields := entries[0].ContextMap() if fields["source"] != "acme_issues" || fields["provider"] != "acme" || fields["kind"] != "issue" || fields["status"] != string(StatusUnauthorized) { t.Fatalf("provider diagnostic fields = %#v", fields) } if strings.Contains(fmt.Sprint(fields), "secret-query") || strings.Contains(fmt.Sprint(fields), "secret upstream detail") { t.Fatalf("provider diagnostic leaked sensitive detail: %#v", fields) } } func TestRegistryRegister_RejectsUnsafeAndDuplicateSources(t *testing.T) { provider := func(source string) fakeProvider { return fakeProvider{ descriptor: ProviderDescriptor{Source: source, Provider: "acme", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return nil, nil }, } } registry := NewRegistry() if err := registry.Register(provider("GitHub Issues")); err == nil { t.Fatal("register unsafe source: err = nil, want validation error") } if err := registry.Register(provider("plugin:acme:issues")); err != nil { t.Fatalf("register valid source: %v", err) } if err := registry.Register(provider("plugin:acme:issues")); err == nil { t.Fatal("register duplicate source: err = nil, want duplicate error") } } func TestRegistryAuthorizesReferenceThroughRegisteredProvider(t *testing.T) { registry := NewRegistry() called := false provider := fakeProvider{ descriptor: ProviderDescriptor{Source: "acme_issues", Provider: "acme", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return nil, nil }, authorize: func(_ context.Context, request ReferenceAuthorizationRequest) error { called = true if request.WorkspaceID != "workspace-1" || request.Purpose != ReferencePurposeSubmission || request.Reference.ID != "issue-7" { t.Fatalf("authorization request = %#v", request) } return nil }, } if err := registry.Register(provider); err != nil { t.Fatalf("register: %v", err) } ref := apiv1.EntityReference{Provider: "acme", Kind: "issue", ID: "issue-7"} if err := registry.AuthorizeReference(context.Background(), ReferenceAuthorizationRequest{ WorkspaceID: "workspace-1", Purpose: ReferencePurposeSubmission, Reference: ref, }); err != nil { t.Fatalf("authorize: %v", err) } if !called { t.Fatal("registered provider authorizer was not called") } if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "acme_issues_duplicate", Provider: "acme", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return nil, nil }, }); err == nil { t.Fatal("duplicate provider/kind registration succeeded") } unknown := ref unknown.Provider = "unknown" if err := registry.AuthorizeReference(context.Background(), ReferenceAuthorizationRequest{ WorkspaceID: "workspace-1", Purpose: ReferencePurposeSubmission, Reference: unknown, }); !errors.Is(err, ErrReferenceProviderUnavailable) { t.Fatalf("unknown provider error = %v, want unavailable", err) } } func TestRegistryRejectsProviderWithoutAuthorizer(t *testing.T) { registry := NewRegistry() err := registry.Register(searchOnlyProvider{descriptor: ProviderDescriptor{ Source: "plain_issues", Provider: "plain", Kind: "issue", }}) if !errors.Is(err, ErrMissingAuthorizer) { t.Fatalf("register error = %v, want missing authorizer", err) } } func TestRegistryReplaceOwnerSwapsDynamicSourcesAndRevokesAuthorization(t *testing.T) { type ownerReplacingRegistry interface { ReplaceOwner(string, ...MentionProvider) error UnregisterOwner(string) error } registry := NewRegistry() owners, ok := any(registry).(ownerReplacingRegistry) if !ok { t.Fatal("registry does not support owner-aware source replacement") } provider := fakeProvider{ descriptor: ProviderDescriptor{ Source: "plugin:acme:pull-requests", Provider: "plugin:acme:bitbucket", Kind: "pull_request", Order: 90, }, search: func(context.Context, SearchRequest) ([]Candidate, error) { return nil, nil }, } if err := owners.ReplaceOwner("kandev-plugin-acme", provider); err != nil { t.Fatalf("register owned source: %v", err) } if got := registry.snapshot(); len(got) != 1 || got[0].descriptor.Source != provider.descriptor.Source { t.Fatalf("snapshot = %#v, want owned source", got) } if err := owners.UnregisterOwner("kandev-plugin-acme"); err != nil { t.Fatalf("unregister owned source: %v", err) } if got := registry.snapshot(); len(got) != 0 { t.Fatalf("snapshot after owner removal = %#v, want empty", got) } err := registry.AuthorizeReference(context.Background(), ReferenceAuthorizationRequest{ WorkspaceID: "workspace-1", Purpose: ReferencePurposeSubmission, Reference: apiv1.EntityReference{Provider: provider.descriptor.Provider, Kind: provider.descriptor.Kind}, }) if !errors.Is(err, ErrReferenceProviderUnavailable) { t.Fatalf("authorization after owner removal = %v, want unavailable", err) } } func TestServiceSearchDropsCandidateRejectedByProviderAuthorizer(t *testing.T) { registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "acme_issues", Provider: "acme", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return []Candidate{ {ID: "allowed", Title: "Allowed", URL: "https://acme.test/allowed", Scope: "tenant"}, {ID: "blocked", Title: "Blocked", URL: "https://evil.test/blocked", Scope: "tenant"}, }, nil }, authorize: func(_ context.Context, request ReferenceAuthorizationRequest) error { if request.Purpose != ReferencePurposeSearch { t.Fatalf("purpose = %q, want search", request.Purpose) } if request.Reference.ID == "blocked" { return errors.New("origin mismatch") } return nil }, }); err != nil { t.Fatalf("register: %v", err) } response, err := NewService(registry).Search(context.Background(), SearchRequest{ WorkspaceID: "workspace-1", Query: "issue", }) if err != nil { t.Fatalf("search: %v", err) } if len(response.Groups[0].Results) != 1 || response.Groups[0].Results[0].ID != "allowed" { t.Fatalf("results = %#v, want only provider-authorized candidate", response.Groups[0].Results) } } func TestServiceSearch_RejectsInvalidWorkspaceAndQuery(t *testing.T) { registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "test", Provider: "test", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { t.Fatal("provider called for invalid request") return nil, nil }, }); err != nil { t.Fatalf("register provider: %v", err) } service := NewService(registry) tests := []SearchRequest{ {WorkspaceID: "", Query: "auth"}, {WorkspaceID: "workspace\x00admin", Query: "auth"}, {WorkspaceID: "workspace-1", Query: " "}, {WorkspaceID: "workspace-1", Query: "auth\x00admin"}, {WorkspaceID: "workspace-1", Query: strings.Repeat("界", 201)}, } for _, request := range tests { if _, err := service.Search(context.Background(), request); err == nil { t.Fatalf("Search(%+v): err = nil, want validation error", request) } } } func TestServiceSearch_ClampsPerSourceLimitAndOutput(t *testing.T) { registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "issues", Provider: "acme", Kind: "issue"}, search: func(_ context.Context, request SearchRequest) ([]Candidate, error) { if request.Limit != MaxLimit { t.Fatalf("provider limit = %d, want %d", request.Limit, MaxLimit) } candidates := make([]Candidate, 0, MaxLimit+2) for i := 0; i < MaxLimit+2; i++ { candidates = append(candidates, Candidate{ ID: fmt.Sprintf("issue-%d", i), Title: fmt.Sprintf("Issue %d", i), URL: fmt.Sprintf("https://issues.example.test/%d", i), Scope: "tenant-a", }) } return candidates, nil }, }); err != nil { t.Fatalf("register provider: %v", err) } response, err := NewService(registry).Search(context.Background(), SearchRequest{ WorkspaceID: "workspace-1", Query: "auth", Limit: 99, }) if err != nil { t.Fatalf("search: %v", err) } if got := len(response.Groups[0].Results); got != MaxLimit { t.Fatalf("result count = %d, want capped %d", got, MaxLimit) } } func TestServiceSearch_DropsUnsafeCandidatesAndDeduplicatesRefs(t *testing.T) { registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "issues", Provider: "acme", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return []Candidate{ {ID: "", Title: "Missing ID", URL: "https://acme.test/issues/missing", Scope: "workspace-1"}, {ID: "bad-scope", Title: "Missing scope", URL: "https://acme.test/issues/bad-scope"}, {ID: "bad-title", URL: "https://acme.test/issues/bad-title", Scope: "workspace-1"}, {ID: "bad-url", Title: "Bad URL", URL: "javascript:alert(1)", Scope: "workspace-1"}, {ID: "issue-1", Key: " ACME-1\n", Title: " Login\n\t failure ", URL: "https://acme.test/issues/1", Scope: " workspace-1 "}, {ID: "issue-1", Key: "ACME-1", Title: "Duplicate", URL: "https://acme.test/issues/1", Scope: "workspace-1"}, }, nil }, }); err != nil { t.Fatalf("register provider: %v", err) } response, err := NewService(registry).Search(context.Background(), SearchRequest{ WorkspaceID: "workspace-1", Query: "login", }) if err != nil { t.Fatalf("search: %v", err) } results := response.Groups[0].Results if len(results) != 1 { t.Fatalf("results = %#v, want one safe unique result", results) } if results[0].Title != "Login failure" || results[0].Key != "ACME-1" || results[0].Scope != "workspace-1" { t.Fatalf("sanitized result = %#v", results[0]) } } func TestServiceSearch_DropsInternalRoutesFromEveryProvider(t *testing.T) { registry := NewRegistry() providers := []fakeProvider{ { descriptor: ProviderDescriptor{Source: "plugin_tasks", Provider: "plugin:acme:tracker", Kind: "task"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return []Candidate{{ID: "spoofed", Title: "Spoofed task", URL: "/t/real-task", Scope: "tenant"}}, nil }, }, { descriptor: ProviderDescriptor{Source: "kandev_tasks", Provider: "kandev", Kind: "task"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return []Candidate{{ID: "task-1", Title: "Task", URL: "/t/task-1", Scope: "workspace-1"}}, nil }, }, } for _, provider := range providers { if err := registry.Register(provider); err != nil { t.Fatalf("register %s: %v", provider.descriptor.Source, err) } } response, err := NewService(registry).Search(context.Background(), SearchRequest{ WorkspaceID: "workspace-1", Query: "task", }) if err != nil { t.Fatalf("search: %v", err) } groups := make(map[string]apiv1.MentionGroup, len(response.Groups)) for _, group := range response.Groups { groups[group.Source] = group } if len(groups["plugin_tasks"].Results) != 0 { t.Fatalf("plugin internal-route results = %#v, want none", groups["plugin_tasks"].Results) } if len(groups["kandev_tasks"].Results) != 0 { t.Fatalf("Kandev internal-route results = %#v, want none", groups["kandev_tasks"].Results) } } func TestServiceSearch_ReturnsDeterministicPartialStatuses(t *testing.T) { registry := NewRegistry() providers := []fakeProvider{ { descriptor: ProviderDescriptor{Source: "success", Provider: "acme", Kind: "issue", Order: 30}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return []Candidate{{ID: "1", Title: "Found", URL: "https://acme.test/1", Scope: "tenant"}}, nil }, }, { descriptor: ProviderDescriptor{Source: "raw-error", Provider: "broken", Kind: "issue", Order: 20}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return nil, errors.New("secret upstream body") }, }, { descriptor: ProviderDescriptor{Source: "auth", Provider: "private", Kind: "ticket", Order: 10}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return nil, NewProviderError(StatusUnauthorized, errors.New("token expired: secret-token")) }, }, } for _, provider := range providers { if err := registry.Register(provider); err != nil { t.Fatalf("register %s: %v", provider.descriptor.Source, err) } } response, err := NewService(registry).Search(context.Background(), SearchRequest{ WorkspaceID: "workspace-1", Query: "auth", }) if err != nil { t.Fatalf("search: %v", err) } if got := []string{response.Groups[0].Source, response.Groups[1].Source, response.Groups[2].Source}; fmt.Sprint(got) != "[auth raw-error success]" { t.Fatalf("group order = %v", got) } if response.Groups[0].Status != StatusUnauthorized || response.Groups[1].Status != StatusUpstreamError || response.Groups[2].Status != StatusOK { t.Fatalf("statuses = %q, %q, %q", response.Groups[0].Status, response.Groups[1].Status, response.Groups[2].Status) } if response.Groups[0].DisplayName != "private" || response.Groups[0].KindLabel != "Work item" { t.Fatalf("generic labels = %#v", response.Groups[0]) } } func TestServiceSearch_ProviderDeadlineBecomesPartialTimeout(t *testing.T) { registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "slow", Provider: "slow", Kind: "issue"}, search: func(ctx context.Context, _ SearchRequest) ([]Candidate, error) { if _, ok := ctx.Deadline(); !ok { return nil, errors.New("provider context missing deadline") } <-ctx.Done() return nil, ctx.Err() }, }); err != nil { t.Fatalf("register provider: %v", err) } response, err := NewService(registry, WithProviderTimeout(10*time.Millisecond)).Search( context.Background(), SearchRequest{WorkspaceID: "workspace-1", Query: "auth"}, ) if err != nil { t.Fatalf("search: %v", err) } if response.Groups[0].Status != StatusTimeout { t.Fatalf("status = %q, want %q", response.Groups[0].Status, StatusTimeout) } } func TestClassifyProviderErrorTreatsCancellationAsTimeout(t *testing.T) { if got := classifyProviderError(context.Canceled); got != StatusTimeout { t.Fatalf("status = %q, want %q", got, StatusTimeout) } } func TestServiceSearch_EnforcesTimeoutWhenProviderIgnoresContext(t *testing.T) { release := make(chan struct{}) t.Cleanup(func() { close(release) }) registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "stuck", Provider: "stuck", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { <-release return nil, nil }, }); err != nil { t.Fatalf("register provider: %v", err) } type outcome struct { response *apiv1.MentionSearchResponse err error } done := make(chan outcome, 1) go func() { response, err := NewService(registry, WithProviderTimeout(10*time.Millisecond)).Search( context.Background(), SearchRequest{WorkspaceID: "workspace-1", Query: "auth"}, ) done <- outcome{response: response, err: err} }() select { case result := <-done: if result.err != nil { t.Fatalf("search: %v", result.err) } if result.response.Groups[0].Status != StatusTimeout { t.Fatalf("status = %q, want %q", result.response.Groups[0].Status, StatusTimeout) } case <-time.After(250 * time.Millisecond): t.Fatal("search remained blocked after provider timeout") } } func TestServiceSearch_BoundsAbandonedProviderCallsPerSource(t *testing.T) { release := make(chan struct{}) t.Cleanup(func() { close(release) }) started := make(chan struct{}, 8) registry := NewRegistry() for index := 0; index < 4; index++ { providerID := fmt.Sprintf("stuck-%d", index) if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: providerID, Provider: providerID, Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { started <- struct{}{} <-release return nil, nil }, }); err != nil { t.Fatalf("register provider: %v", err) } } service := NewService( registry, WithMaxConcurrency(1), WithProviderTimeout(10*time.Millisecond), ) for attempt := 0; attempt < 4; attempt++ { response, err := service.Search( context.Background(), SearchRequest{WorkspaceID: "workspace-1", Query: "auth"}, ) if err != nil { t.Fatalf("search %d: %v", attempt, err) } for _, group := range response.Groups { if group.Status != StatusTimeout { t.Fatalf("search %d group status = %q", attempt, group.Status) } } } if got := len(started); got != 4 { t.Fatalf("started provider calls = %d, want one quarantined call per source", got) } } func TestServiceSearch_QuarantinesRepeatedStuckProviderWithoutHidingHealthyResults(t *testing.T) { release := make(chan struct{}) t.Cleanup(func() { close(release) }) stuckStarted := make(chan struct{}, 4) registry := NewRegistry() if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "stuck", Provider: "stuck", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { stuckStarted <- struct{}{} <-release return nil, nil }, }); err != nil { t.Fatalf("register stuck provider: %v", err) } if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: "healthy", Provider: "healthy", Kind: "issue"}, search: func(context.Context, SearchRequest) ([]Candidate, error) { return []Candidate{{ ID: "healthy-1", Title: "Healthy result", URL: "https://healthy.example.test/1", Scope: "workspace-1", }}, nil }, }); err != nil { t.Fatalf("register healthy provider: %v", err) } service := NewService( registry, WithMaxConcurrency(1), WithProviderTimeout(10*time.Millisecond), ) for attempt := 0; attempt < 4; attempt++ { response, err := service.Search( context.Background(), SearchRequest{WorkspaceID: "workspace-1", Query: "auth"}, ) if err != nil { t.Fatalf("search %d: %v", attempt, err) } groups := make(map[string]apiv1.MentionGroup, len(response.Groups)) for _, group := range response.Groups { groups[group.Source] = group } if groups["stuck"].Status != StatusTimeout { t.Fatalf("search %d stuck status = %q, want %q", attempt, groups["stuck"].Status, StatusTimeout) } if groups["healthy"].Status != StatusOK || len(groups["healthy"].Results) != 1 { t.Fatalf("search %d healthy group = %#v, want successful result", attempt, groups["healthy"]) } } if got := len(stuckStarted); got != 1 { t.Fatalf("started stuck provider calls = %d, want quarantined at 1", got) } } func TestServiceSearch_BoundsConcurrentFanoutAndKeepsDescriptorOrder(t *testing.T) { started := make(chan string, 3) release := make(chan struct{}) var releaseOnce sync.Once t.Cleanup(func() { releaseOnce.Do(func() { close(release) }) }) registry := NewRegistry() for index, source := range []string{"c", "a", "b"} { source := source if err := registry.Register(fakeProvider{ descriptor: ProviderDescriptor{Source: source, Provider: source, Kind: "issue", Order: 30 - index*10}, search: func(context.Context, SearchRequest) ([]Candidate, error) { started <- source <-release return nil, nil }, }); err != nil { t.Fatalf("register %s: %v", source, err) } } type searchOutcome struct { responseSources []string err error } done := make(chan searchOutcome, 1) go func() { response, err := NewService( registry, WithMaxConcurrency(2), WithProviderTimeout(time.Second), ).Search(context.Background(), SearchRequest{WorkspaceID: "workspace-1", Query: "auth"}) outcome := searchOutcome{err: err} if response != nil { for _, group := range response.Groups { outcome.responseSources = append(outcome.responseSources, group.Source) } } done <- outcome }() for i := 0; i < 2; i++ { select { case <-started: case <-time.After(500 * time.Millisecond): t.Fatal("fewer than two providers started concurrently") } } select { case source := <-started: t.Fatalf("provider %q exceeded concurrency bound before release", source) case <-time.After(50 * time.Millisecond): } releaseOnce.Do(func() { close(release) }) select { case outcome := <-done: if outcome.err != nil { t.Fatalf("search: %v", outcome.err) } if got := fmt.Sprint(outcome.responseSources); got != "[b a c]" { t.Fatalf("group order = %s, want [b a c]", got) } case <-time.After(time.Second): t.Fatal("search did not finish after providers released") } }