Files
kandev/apps/backend/internal/github/workspace_settings_test.go
T
2026-07-27 18:10:12 +01:00

786 lines
27 KiB
Go

package github
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"testing"
"time"
)
func TestStore_GitHubWorkspaceSettingsRoundTrip(t *testing.T) {
store := newTestStore(t)
ctx := context.Background()
input := &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeOrgs: []string{"kdlbs"},
RepoScopeRepos: []RepoFilter{
{Owner: "kdlbs", Name: "kandev"},
},
SavedPresets: []byte(`[{"id":"p1","kind":"pr","label":"Mine"}]`),
DefaultQueryPresets: []byte(`{"pr":[],"issue":[]}`),
}
if err := store.UpsertWorkspaceSettings(ctx, input); err != nil {
t.Fatalf("upsert workspace settings: %v", err)
}
got, err := store.GetWorkspaceSettings(ctx, "ws-1")
if err != nil {
t.Fatalf("get workspace settings: %v", err)
}
if got.WorkspaceID != "ws-1" || got.RepoScopeMode != RepoScopeModeRepos {
t.Fatalf("unexpected settings identity/scope: %+v", got)
}
if len(got.RepoScopeRepos) != 1 || got.RepoScopeRepos[0].Owner != "kdlbs" || got.RepoScopeRepos[0].Name != "kandev" {
t.Fatalf("repo scope lost on round trip: %+v", got.RepoScopeRepos)
}
if string(got.SavedPresets) != string(input.SavedPresets) {
t.Fatalf("saved presets = %s, want %s", got.SavedPresets, input.SavedPresets)
}
if string(got.DefaultQueryPresets) != string(input.DefaultQueryPresets) {
t.Fatalf("default query presets = %s, want %s", got.DefaultQueryPresets, input.DefaultQueryPresets)
}
}
func TestTaskGitCredentialModeDefaultsManaged(t *testing.T) {
store := newTestStore(t)
settings, err := store.GetWorkspaceSettings(context.Background(), "ws-credential-mode")
if err != nil {
t.Fatalf("GetWorkspaceSettings() error = %v", err)
}
raw, err := json.Marshal(settings)
if err != nil {
t.Fatalf("marshal workspace settings: %v", err)
}
if !strings.Contains(string(raw), `"task_git_credentials_mode":"managed"`) {
t.Fatalf("settings JSON = %s, want managed task Git credential mode", raw)
}
}
func TestDescribeTaskGitCredentialPolicyUsesConnectionIdentity(t *testing.T) {
store := newTestStore(t)
ctx := context.Background()
installationID := int64(42)
for _, tc := range []struct {
name string
conn WorkspaceConnection
method string
actor string
}{
{
name: "PAT", conn: WorkspaceConnection{WorkspaceID: "ws-pat", Source: ConnectionSourcePAT, GitHubHost: defaultGitHubHost, Login: "alice", Status: ConnectionStatusActive},
method: "pat", actor: "alice",
},
{
name: "GitHub App", conn: WorkspaceConnection{WorkspaceID: "ws-app", Source: ConnectionSourceGitHubAppInstallation, GitHubHost: defaultGitHubHost, InstallationID: &installationID, InstallationAccountLogin: "acme-bot", InstallationAccountType: "Organization", AppRegistrationID: "app-1", Status: ConnectionStatusActive},
method: "github_app_installation", actor: "acme-bot",
},
} {
t.Run(tc.name, func(t *testing.T) {
if _, err := store.db.ExecContext(ctx, `INSERT INTO workspaces (id) VALUES (?)`, tc.conn.WorkspaceID); err != nil {
t.Fatalf("create workspace: %v", err)
}
if tc.conn.AppRegistrationID != "" {
registration := newAppRegistration(tc.conn.AppRegistrationID, 99, "Acme automation", time.Now().UTC())
if err := store.InsertAppRegistration(ctx, registration); err != nil {
t.Fatalf("create app registration: %v", err)
}
}
if err := store.UpsertWorkspaceConnection(ctx, &tc.conn); err != nil {
t.Fatalf("UpsertWorkspaceConnection() error = %v", err)
}
svc := newWorkspaceAuthenticatedTestService(t, NewMockClient(), store, tc.conn.WorkspaceID)
policy, err := svc.DescribeTaskGitCredentialPolicy(ctx, tc.conn.WorkspaceID)
if err != nil {
t.Fatalf("DescribeTaskGitCredentialPolicy() error = %v", err)
}
if policy.Mode != TaskGitCredentialsModeManaged || policy.WorkspaceMethod != tc.method || policy.WorkspaceActor != tc.actor {
t.Fatalf("policy = %+v", policy)
}
})
}
}
func TestService_UpdateWorkspaceSettings_TaskGitCredentialMode(t *testing.T) {
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, NewMockClient(), store, "ws-credential-mode")
executor := TaskGitCredentialsModeExecutor
updated, err := svc.UpdateWorkspaceSettings(context.Background(), &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-credential-mode",
TaskGitCredentialsMode: &executor,
})
if err != nil {
t.Fatalf("UpdateWorkspaceSettings() error = %v", err)
}
if got := updated.TaskGitCredentialsMode; got != TaskGitCredentialsModeExecutor {
t.Fatalf("TaskGitCredentialsMode = %q, want executor", got)
}
invalid := "host"
_, err = svc.UpdateWorkspaceSettings(context.Background(), &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-credential-mode",
TaskGitCredentialsMode: &invalid,
})
if !errors.Is(err, ErrWorkspaceSettingsValidation) {
t.Fatalf("UpdateWorkspaceSettings() error = %v, want validation error", err)
}
}
func TestService_SearchUserPRsPagedForWorkspace_FiltersToSelectedRepos(t *testing.T) {
client := NewMockClient()
client.AddPR(&PR{RepoOwner: "kdlbs", RepoName: "kandev", Number: 1, Title: "in scope"})
client.AddPR(&PR{RepoOwner: "other", RepoName: "repo", Number: 2, Title: "out of scope"})
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeRepos: []RepoFilter{{Owner: "kdlbs", Name: "kandev"}},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
page, err := svc.SearchUserPRsPagedForWorkspace(ctx, "ws-1", "", "repo:other/repo is:open", 1, 25)
if err != nil {
t.Fatalf("search scoped prs: %v", err)
}
if page.TotalCount != 2 || len(page.PRs) != 1 {
t.Fatalf("expected one scoped PR, got total=%d prs=%+v", page.TotalCount, page.PRs)
}
if page.PRs[0].RepoOwner != "kdlbs" || page.PRs[0].RepoName != "kandev" {
t.Fatalf("custom query escaped workspace scope: %+v", page.PRs[0])
}
}
func TestService_SearchUserPRsPagedForWorkspace_AppendsScopeToQuery(t *testing.T) {
client := &capturingSearchClient{MockClient: NewMockClient()}
client.AddPR(&PR{RepoOwner: "kdlbs", RepoName: "kandev", Number: 1, Title: "in scope"})
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeRepos: []RepoFilter{
{Owner: "kdlbs", Name: "kandev"},
{Owner: "kdlbs", Name: "docs"},
},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
page, err := svc.SearchUserPRsPagedForWorkspace(ctx, "ws-1", "", "is:open", 1, 25)
if err != nil {
t.Fatalf("search scoped prs: %v", err)
}
if page.TotalCount != 1 || len(page.PRs) != 1 {
t.Fatalf("expected one scoped PR, got total=%d prs=%+v", page.TotalCount, page.PRs)
}
assertSearchCalls(t, client.calls, "is:open", []string{"repo:kdlbs/kandev", "repo:kdlbs/docs"})
client.calls = nil
if _, err := svc.SearchUserPRsPagedForWorkspace(ctx, "ws-1", "review-requested:@me", "", 1, 25); err != nil {
t.Fatalf("search scoped prs with preset filter: %v", err)
}
assertSearchCalls(t, client.calls, "review-requested:@me", []string{"repo:kdlbs/kandev", "repo:kdlbs/docs"})
}
func assertSearchCalls(t *testing.T, calls []searchCall, baseQuery string, qualifiers []string) {
t.Helper()
if len(calls) != len(qualifiers) {
t.Fatalf("search calls = %+v, want %d calls", calls, len(qualifiers))
}
for i, qualifier := range qualifiers {
query := strings.TrimSpace(calls[i].filter + " " + calls[i].customQuery)
if !strings.Contains(query, baseQuery) || !strings.Contains(query, qualifier) {
t.Fatalf("search call %d query = %q, want %q and %q", i, query, baseQuery, qualifier)
}
if strings.Contains(query, " OR ") || strings.ContainsAny(query, "()") {
t.Fatalf("search call %d should not OR qualifiers: %q", i, query)
}
}
}
func assertHasSearchCall(t *testing.T, calls []searchCall, qualifier string, page, perPage int) {
t.Helper()
for _, call := range calls {
query := strings.TrimSpace(call.filter + " " + call.customQuery)
if strings.Contains(query, qualifier) && call.page == page && call.perPage == perPage {
return
}
}
t.Fatalf("search calls = %+v, want %s page=%d perPage=%d", calls, qualifier, page, perPage)
}
func TestService_SearchUserPRsPagedForWorkspace_OrgScopeIncludesPersonalOwner(t *testing.T) {
client := &capturingSearchClient{MockClient: NewMockClient()}
client.AddPR(&PR{RepoOwner: "octo", RepoName: "personal", Number: 1, Title: "in scope"})
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeOrgs,
RepoScopeOrgs: []string{"octo"},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
if _, err := svc.SearchUserPRsPagedForWorkspace(ctx, "ws-1", "", "is:open", 1, 25); err != nil {
t.Fatalf("search scoped prs: %v", err)
}
assertSearchCalls(t, client.calls, "is:open", []string{"org:octo", "user:octo"})
}
func TestService_SearchUserPRsPagedForWorkspace_FanoutFetchesDeeperProviderPages(t *testing.T) {
client := &capturingSearchClient{MockClient: NewMockClient()}
for i := 1; i <= 150; i++ {
client.AddPR(&PR{RepoOwner: "kdlbs", RepoName: "kandev", Number: i, Title: fmt.Sprintf("kandev-%d", i)})
}
for i := 1; i <= 10; i++ {
client.AddPR(&PR{RepoOwner: "example", RepoName: "api", Number: i, Title: fmt.Sprintf("api-%d", i)})
}
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeRepos: []RepoFilter{
{Owner: "kdlbs", Name: "kandev"},
{Owner: "example", Name: "api"},
},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
page, err := svc.SearchUserPRsPagedForWorkspace(ctx, "ws-1", "", "is:open", 5, 25)
if err != nil {
t.Fatalf("search scoped prs: %v", err)
}
if page.TotalCount != 160 {
t.Fatalf("total count = %d, want 160", page.TotalCount)
}
if len(page.PRs) != 25 {
t.Fatalf("page PR count = %d, want 25", len(page.PRs))
}
if page.PRs[0].RepoOwner != "kdlbs" || page.PRs[0].RepoName != "kandev" || page.PRs[0].Number != 101 {
t.Fatalf("first PR on page = %+v, want kdlbs/kandev#101", page.PRs[0])
}
if page.PRs[24].RepoOwner != "kdlbs" || page.PRs[24].RepoName != "kandev" || page.PRs[24].Number != 125 {
t.Fatalf("last PR on page = %+v, want kdlbs/kandev#125", page.PRs[24])
}
assertHasSearchCall(t, client.calls, "repo:kdlbs/kandev", 2, 100)
}
func TestService_SearchUserPRsPagedForWorkspace_OrgScopeDedupesFanoutTotal(t *testing.T) {
client := &capturingSearchClient{MockClient: NewMockClient()}
for i := 1; i <= 120; i++ {
client.AddPR(&PR{RepoOwner: "octo", RepoName: "repo", Number: i, Title: fmt.Sprintf("octo-%d", i)})
}
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeOrgs,
RepoScopeOrgs: []string{"octo"},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
page, err := svc.SearchUserPRsPagedForWorkspace(ctx, "ws-1", "", "is:open", 1, 25)
if err != nil {
t.Fatalf("search scoped prs: %v", err)
}
if page.TotalCount != 120 {
t.Fatalf("total count = %d, want 120", page.TotalCount)
}
if len(page.PRs) != 25 {
t.Fatalf("page PR count = %d, want 25", len(page.PRs))
}
assertSearchCalls(t, client.calls, "is:open", []string{"org:octo", "user:octo"})
}
func TestService_SearchUserPRsPagedForWorkspace_EmptyRepoScopeFailsClosed(t *testing.T) {
client := NewMockClient()
client.AddPR(&PR{RepoOwner: "kdlbs", RepoName: "kandev", Number: 1, Title: "hidden"})
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeRepos: []RepoFilter{},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
page, err := svc.SearchUserPRsPagedForWorkspace(ctx, "ws-1", "", "is:open", 1, 25)
if err != nil {
t.Fatalf("search scoped prs: %v", err)
}
if page.TotalCount != 0 || len(page.PRs) != 0 {
t.Fatalf("empty repo scope should return no PRs, got total=%d prs=%+v", page.TotalCount, page.PRs)
}
}
func TestService_UpdateWorkspaceSettings_PartialUpdateAndNullClear(t *testing.T) {
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, NewMockClient(), store, "ws-1")
ctx := context.Background()
defaults := json.RawMessage(`{"pr":[{"value":"mine","label":"Mine","filter":"is:open","group":"inbox"}],"issue":[]}`)
saved := json.RawMessage(`[{"id":"p1","kind":"pr","label":"Mine"}]`)
mode := RepoScopeModeRepos
repos := []RepoFilter{{Owner: "kdlbs", Name: "kandev"}}
if _, err := svc.UpdateWorkspaceSettings(ctx, &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-1",
RepoScopeMode: &mode,
RepoScopeRepos: &repos,
SavedPresets: &saved,
SavedPresetsSet: true,
DefaultQueryPresets: &defaults,
DefaultQueriesSet: true,
}); err != nil {
t.Fatalf("initial update: %v", err)
}
nextSaved := json.RawMessage(`[{"id":"p2","kind":"issue","label":"Issues"}]`)
got, err := svc.UpdateWorkspaceSettings(ctx, &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-1",
SavedPresets: &nextSaved,
SavedPresetsSet: true,
})
if err != nil {
t.Fatalf("partial saved presets update: %v", err)
}
if string(got.SavedPresets) != string(nextSaved) {
t.Fatalf("saved presets = %s, want %s", got.SavedPresets, nextSaved)
}
if string(got.DefaultQueryPresets) != string(defaults) {
t.Fatalf("default presets should be preserved, got %s", got.DefaultQueryPresets)
}
got, err = svc.UpdateWorkspaceSettings(ctx, &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-1",
DefaultQueriesSet: true,
})
if err != nil {
t.Fatalf("clear default query presets: %v", err)
}
if got.DefaultQueryPresets != nil {
t.Fatalf("default presets should be cleared, got %s", got.DefaultQueryPresets)
}
if string(got.SavedPresets) != string(nextSaved) {
t.Fatalf("saved presets should be preserved after clear, got %s", got.SavedPresets)
}
got, err = svc.UpdateWorkspaceSettings(ctx, &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-1",
SavedPresetsSet: true,
})
if err != nil {
t.Fatalf("clear saved presets: %v", err)
}
if string(got.SavedPresets) != "[]" {
t.Fatalf("saved presets should be cleared to empty array, got %s", got.SavedPresets)
}
}
func TestUpdateWorkspaceSettingsRequest_UnmarshalTracksExplicitNull(t *testing.T) {
var req UpdateWorkspaceSettingsRequest
if err := json.Unmarshal([]byte(`{"workspace_id":"ws-1","default_query_presets":null}`), &req); err != nil {
t.Fatalf("unmarshal request: %v", err)
}
if !req.DefaultQueriesSet {
t.Fatal("expected default query presets to be marked present")
}
if req.DefaultQueryPresets != nil {
t.Fatalf("expected explicit null to leave raw pointer nil, got %s", *req.DefaultQueryPresets)
}
}
func TestUpdateWorkspaceSettingsRequest_UnmarshalTracksSavedPresetsNull(t *testing.T) {
var req UpdateWorkspaceSettingsRequest
if err := json.Unmarshal([]byte(`{"workspace_id":"ws-1","saved_presets":null}`), &req); err != nil {
t.Fatalf("unmarshal request: %v", err)
}
if !req.SavedPresetsSet {
t.Fatal("expected saved presets to be marked present")
}
if req.SavedPresets != nil {
t.Fatalf("expected explicit null to leave raw pointer nil, got %s", *req.SavedPresets)
}
}
func TestService_UpdateWorkspaceSettings_RejectsUnknownRepoScopeMode(t *testing.T) {
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, NewMockClient(), store, "ws-1")
mode := "everything"
_, err := svc.UpdateWorkspaceSettings(context.Background(), &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-1",
RepoScopeMode: &mode,
})
if !errors.Is(err, ErrWorkspaceSettingsValidation) {
t.Fatalf("expected validation error, got %v", err)
}
}
func TestService_UpdateWorkspaceSettings_RejectsConflictingScopePatch(t *testing.T) {
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, NewMockClient(), store, "ws-1")
mode := RepoScopeModeAll
orgs := []string{"octo"}
_, err := svc.UpdateWorkspaceSettings(context.Background(), &UpdateWorkspaceSettingsRequest{
WorkspaceID: "ws-1",
RepoScopeMode: &mode,
RepoScopeOrgs: &orgs,
})
if !errors.Is(err, ErrWorkspaceSettingsValidation) {
t.Fatalf("expected validation error, got %v", err)
}
}
func TestService_SearchUserIssuesPagedForWorkspace_AllScopePreservesResults(t *testing.T) {
client := &issueSearchClient{
issues: []*Issue{
{RepoOwner: "kdlbs", RepoName: "kandev", Number: 1, Title: "one"},
{RepoOwner: "other", RepoName: "repo", Number: 2, Title: "two"},
},
}
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
page, err := svc.SearchUserIssuesPagedForWorkspace(context.Background(), "ws-1", "", "is:open", 1, 25)
if err != nil {
t.Fatalf("search scoped issues: %v", err)
}
if page.TotalCount != 2 || len(page.Issues) != 2 {
t.Fatalf("all scope should preserve results, got total=%d issues=%+v", page.TotalCount, page.Issues)
}
}
func TestService_SearchUserIssuesPagedForWorkspace_FansOutSelectedRepos(t *testing.T) {
client := &rejectingORIssueSearchClient{
MockClient: NewMockClient(),
issuesByRepo: map[string][]*Issue{
"kdlbs/kandev": {
{RepoOwner: "kdlbs", RepoName: "kandev", Number: 1, Title: "one"},
},
"example/api": {
{RepoOwner: "example", RepoName: "api", Number: 2, Title: "two"},
},
},
}
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeRepos: []RepoFilter{
{Owner: "kdlbs", Name: "kandev"},
{Owner: "example", Name: "api"},
},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
page, err := svc.SearchUserIssuesPagedForWorkspace(ctx, "ws-1", "", "is:open", 1, 25)
if err != nil {
t.Fatalf("search scoped issues: %v", err)
}
if page.TotalCount != 2 || len(page.Issues) != 2 {
t.Fatalf("expected both selected repo issues, got total=%d issues=%+v", page.TotalCount, page.Issues)
}
if len(client.calls) != 2 {
t.Fatalf("expected one provider search per selected repo, got calls=%+v", client.calls)
}
for _, call := range client.calls {
query := strings.TrimSpace(call.filter + " " + call.customQuery)
if strings.Contains(query, " OR ") || strings.ContainsAny(query, "()") {
t.Fatalf("provider query should not OR repo qualifiers: %q", query)
}
}
}
func TestService_CheckReviewWatch_AppliesWorkspaceRepoScope(t *testing.T) {
client := NewMockClient()
client.AddPR(&PR{
RepoOwner: "kdlbs",
RepoName: "kandev",
Number: 1,
Title: "in scope",
RequestedReviewers: []RequestedReviewer{{Login: "octo", Type: "user"}},
})
client.AddPR(&PR{
RepoOwner: "other",
RepoName: "repo",
Number: 2,
Title: "out of scope",
RequestedReviewers: []RequestedReviewer{{Login: "octo", Type: "user"}},
})
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeRepos: []RepoFilter{{Owner: "kdlbs", Name: "kandev"}},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
prs, err := svc.CheckReviewWatch(ctx, &ReviewWatch{
ID: "watch-1",
WorkspaceID: "ws-1",
Repos: nil,
ReviewScope: ReviewScopeUserAndTeams,
Enabled: true,
PollIntervalSeconds: 300,
})
if err != nil {
t.Fatalf("check review watch: %v", err)
}
if len(prs) != 1 || prs[0].RepoOwner != "kdlbs" || prs[0].RepoName != "kandev" {
t.Fatalf("expected one in-scope PR, got %+v", prs)
}
}
func TestService_CheckReviewWatch_EmptyWorkspaceScopeSkipsProviderFetch(t *testing.T) {
client := &countingReviewClient{MockClient: NewMockClient()}
client.AddPR(&PR{
RepoOwner: "kdlbs",
RepoName: "kandev",
Number: 1,
Title: "hidden",
RequestedReviewers: []RequestedReviewer{{Login: "octo", Type: "user"}},
})
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeRepos,
RepoScopeRepos: []RepoFilter{},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
prs, err := svc.CheckReviewWatch(ctx, &ReviewWatch{
ID: "watch-1",
WorkspaceID: "ws-1",
Repos: nil,
ReviewScope: ReviewScopeUserAndTeams,
Enabled: true,
PollIntervalSeconds: 300,
})
if err != nil {
t.Fatalf("check review watch: %v", err)
}
if len(prs) != 0 {
t.Fatalf("empty workspace scope should return no PRs, got %+v", prs)
}
if client.listReviewRequestedCalls != 0 {
t.Fatalf("expected no broad provider fetch, got %d calls", client.listReviewRequestedCalls)
}
}
func TestService_CheckIssueWatch_AppliesWorkspaceRepoScope(t *testing.T) {
client := &issueSearchClient{
issues: []*Issue{
{RepoOwner: "kdlbs", RepoName: "kandev", Number: 1, Title: "in scope"},
{RepoOwner: "other", RepoName: "repo", Number: 2, Title: "out of scope"},
},
}
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeOrgs,
RepoScopeOrgs: []string{"kdlbs"},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
issues, err := svc.CheckIssueWatch(ctx, &IssueWatch{
ID: "watch-1",
WorkspaceID: "ws-1",
Repos: nil,
Enabled: true,
PollIntervalSeconds: 300,
})
if err != nil {
t.Fatalf("check issue watch: %v", err)
}
if len(issues) != 1 || issues[0].RepoOwner != "kdlbs" || issues[0].RepoName != "kandev" {
t.Fatalf("expected one in-scope issue, got %+v", issues)
}
}
func TestService_CheckIssueWatch_EmptyWorkspaceScopeSkipsProviderFetch(t *testing.T) {
client := &countingWorkspaceIssueClient{MockClient: NewMockClient()}
store := newTestStore(t)
svc := newWorkspaceAuthenticatedTestService(t, client, store, "ws-1")
ctx := context.Background()
if err := svc.UpsertWorkspaceSettings(ctx, &WorkspaceSettings{
WorkspaceID: "ws-1",
RepoScopeMode: RepoScopeModeOrgs,
RepoScopeOrgs: []string{},
}); err != nil {
t.Fatalf("save workspace settings: %v", err)
}
issues, err := svc.CheckIssueWatch(ctx, &IssueWatch{
ID: "watch-1",
WorkspaceID: "ws-1",
Repos: nil,
Enabled: true,
PollIntervalSeconds: 300,
})
if err != nil {
t.Fatalf("check issue watch: %v", err)
}
if len(issues) != 0 {
t.Fatalf("empty workspace scope should return no issues, got %+v", issues)
}
if client.listIssuesCalls != 0 {
t.Fatalf("expected no broad provider fetch, got %d calls", client.listIssuesCalls)
}
}
type issueSearchClient struct {
*MockClient
issues []*Issue
}
type countingReviewClient struct {
*MockClient
listReviewRequestedCalls int
}
func (c *countingReviewClient) ListReviewRequestedPRs(ctx context.Context, reviewScope, owner, repo string) ([]*PR, error) {
c.listReviewRequestedCalls++
return c.MockClient.ListReviewRequestedPRs(ctx, reviewScope, owner, repo)
}
type countingWorkspaceIssueClient struct {
*MockClient
listIssuesCalls int
}
func (c *countingWorkspaceIssueClient) ListIssues(context.Context, string, string) ([]*Issue, error) {
c.listIssuesCalls++
return []*Issue{{RepoOwner: "kdlbs", RepoName: "kandev", Number: 1, Title: "hidden"}}, nil
}
type capturingSearchClient struct {
*MockClient
filter string
customQuery string
calls []searchCall
}
type searchCall struct {
filter string
customQuery string
page int
perPage int
}
type rejectingORIssueSearchClient struct {
*MockClient
issuesByRepo map[string][]*Issue
calls []searchCall
}
func (c *capturingSearchClient) SearchPRsPaged(ctx context.Context, filter, customQuery string, page, perPage int) (*PRSearchPage, error) {
c.filter = filter
c.customQuery = customQuery
c.calls = append(c.calls, searchCall{filter: filter, customQuery: customQuery, page: page, perPage: perPage})
result, err := c.MockClient.SearchPRsPaged(ctx, filter, customQuery, page, perPage)
if err != nil {
return nil, err
}
return filterPRSearchPageByQuery(result, strings.TrimSpace(filter+" "+customQuery), page, perPage), nil
}
func (c *issueSearchClient) ListIssues(context.Context, string, string) ([]*Issue, error) {
return c.issues, nil
}
func (c *issueSearchClient) ListIssuesPaged(context.Context, string, string, int, int) (*IssueSearchPage, error) {
return &IssueSearchPage{Issues: c.issues, TotalCount: len(c.issues), Page: 1, PerPage: 25}, nil
}
func (c *rejectingORIssueSearchClient) ListIssuesPaged(_ context.Context, filter, customQuery string, page, perPage int) (*IssueSearchPage, error) {
c.calls = append(c.calls, searchCall{filter: filter, customQuery: customQuery, page: page, perPage: perPage})
query := strings.TrimSpace(filter + " " + customQuery)
if strings.Contains(query, " OR ") || strings.ContainsAny(query, "()") {
return nil, fmt.Errorf("github rejected invalid qualifier query %q", query)
}
for repo, issues := range c.issuesByRepo {
if strings.Contains(query, "repo:"+repo) {
return &IssueSearchPage{Issues: paginateSearchResults(issues, page, perPage), TotalCount: len(issues), Page: page, PerPage: perPage}, nil
}
}
return &IssueSearchPage{Issues: []*Issue{}, TotalCount: 0, Page: page, PerPage: perPage}, nil
}
func filterPRSearchPageByQuery(page *PRSearchPage, query string, pageNum, perPage int) *PRSearchPage {
if page == nil {
return nil
}
if !strings.Contains(query, "repo:") && !strings.Contains(query, "org:") && !strings.Contains(query, "user:") {
return page
}
prs := make([]*PR, 0, len(page.PRs))
for _, pr := range page.PRs {
if prMatchesQueryQualifier(pr, query) {
prs = append(prs, pr)
}
}
sort.Slice(prs, func(i, j int) bool {
left := strings.ToLower(prs[i].RepoOwner + "/" + prs[i].RepoName)
right := strings.ToLower(prs[j].RepoOwner + "/" + prs[j].RepoName)
if left != right {
return left < right
}
return prs[i].Number < prs[j].Number
})
return &PRSearchPage{PRs: paginateSearchResults(prs, pageNum, perPage), TotalCount: len(prs), Page: pageNum, PerPage: perPage}
}
func prMatchesQueryQualifier(pr *PR, query string) bool {
if pr == nil {
return false
}
owner := strings.ToLower(strings.TrimSpace(pr.RepoOwner))
name := strings.ToLower(strings.TrimSpace(pr.RepoName))
query = strings.ToLower(query)
return strings.Contains(query, fmt.Sprintf("repo:%s/%s", owner, name)) ||
strings.Contains(query, "org:"+owner) ||
strings.Contains(query, "user:"+owner)
}