Files
kandev/apps/backend/internal/backendapp/task_change_link_identity_test.go
T
2026-09-15 17:48:39 +01:00

151 lines
5.8 KiB
Go

package backendapp
import (
"context"
"errors"
"testing"
"github.com/jmoiron/sqlx"
"github.com/kandev/kandev/internal/auth"
"github.com/kandev/kandev/internal/auth/authn"
authstore "github.com/kandev/kandev/internal/auth/store"
"github.com/kandev/kandev/internal/common/config"
"github.com/kandev/kandev/internal/github"
mcphandlers "github.com/kandev/kandev/internal/mcp/handlers"
userstore "github.com/kandev/kandev/internal/user/store"
"github.com/stretchr/testify/require"
)
// @covers AC-INTEGRATIONS-TASK-CHANGE-LINK-MCP-001.1
func TestTaskChangeCoordinatorGitHubIdentityModes(t *testing.T) {
disabled := newDisabledAuthService(t)
require.Equal(t, auth.ModeDisabled, disabled.Mode())
cfg := &config.Config{}
cfg.Features.Auth = true
enabled := newEnabledAuthService(t, cfg)
setup := newTaskChangeSetupAuthService(t)
identity := &authn.Identity{UserID: "user-1", Role: authn.RoleMember, TokenID: "token-1"}
for _, tc := range []struct {
name string
svc *auth.Service
identity *authn.Identity
wantUser string
noResolver bool
}{
{name: "disabled", svc: disabled, wantUser: userstore.DefaultUserID},
{name: "enabled rejects missing identity", svc: enabled},
{name: "setup rejects missing identity", svc: setup},
{name: "unavailable rejects missing identity"},
{name: "unwired resolver rejects missing identity", noResolver: true},
{name: "disabled preserves real identity", svc: disabled, identity: identity, wantUser: "user-1"},
{name: "enabled preserves real identity", svc: enabled, identity: identity, wantUser: "user-1"},
{name: "unavailable preserves real identity", identity: identity, wantUser: "user-1"},
{name: "empty identity rejects fallback", svc: disabled, identity: &authn.Identity{}},
{name: "blank identity rejects fallback", svc: disabled, identity: &authn.Identity{UserID: " \t"}},
} {
t.Run(tc.name, func(t *testing.T) {
taskSvc, repos := newTaskChangeCoordinatorHarness(t)
seedTaskChangeCoordinatorTask(t, repos, "ws-1", "task-1", "repo-1", "https://github.com", "acme", "api")
provider := &fakeGitHubChangeLinks{}
coordinator := taskChangeLinkCoordinator{
tasks: taskSvc, github: provider, singleUserIdentity: taskChangeLinkIdentityResolver(tc.svc),
}
if tc.noResolver {
coordinator.singleUserIdentity = nil
}
ctx := context.Background()
if tc.identity != nil {
ctx = authn.WithIdentity(ctx, *tc.identity)
}
links, err := coordinator.LinkTaskChange(ctx, mcphandlers.TaskChangeLinkRequest{
TaskID: "task-1", Link: mcphandlers.TaskChangeLink{Provider: "github", RepositoryID: "repo-1", Number: 42},
})
if tc.wantUser == "" {
require.EqualError(t, err, "authenticated user identity is required for GitHub PR links")
require.Empty(t, links)
require.Empty(t, provider.linkedURL)
require.Empty(t, provider.prs)
return
}
require.NoError(t, err)
require.Len(t, links, 1)
require.Equal(t, tc.wantUser, provider.linkedUserID)
require.Equal(t, "ws-1", provider.linkedWorkspaceID)
gotIdentity, ok := authn.IdentityFromContext(ctx)
require.Equal(t, tc.identity != nil, ok)
if tc.identity != nil {
require.Equal(t, *tc.identity, gotIdentity)
}
})
}
}
func newTaskChangeSetupAuthService(t *testing.T) *auth.Service {
t.Helper()
conn, err := sqlx.Open("sqlite3", ":memory:")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, conn.Close()) })
users, cleanup, err := userstore.Provide(conn, conn)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, cleanup()) })
store, err := authstore.New(conn, conn)
require.NoError(t, err)
cfg := &config.Config{}
cfg.Features.Auth = true
svc, err := auth.NewService(context.Background(), auth.Deps{Cfg: cfg, Store: store, Users: users})
require.NoError(t, err)
require.Equal(t, auth.ModeSetup, svc.Mode())
return svc
}
// @covers AC-INTEGRATIONS-TASK-CHANGE-LINK-MCP-001.3
// @covers AC-INTEGRATIONS-TASK-CHANGE-LINK-MCP-001.4
func TestTaskChangeCoordinatorGitHubReplacementSingleUser(t *testing.T) {
for _, tc := range []struct {
name string
same bool
linkErr error
}{
{name: "replaces old link"},
{name: "same link is a no-op", same: true},
{name: "provider failure preserves old link", linkErr: errors.New("provider unavailable")},
} {
t.Run(tc.name, func(t *testing.T) {
taskSvc, repos := newTaskChangeCoordinatorHarness(t)
seedTaskChangeCoordinatorTask(t, repos, "ws-1", "task-1", "repo-1", "https://github.com", "acme", "api")
old := mcphandlers.TaskChangeLink{Provider: "github", RepositoryID: "repo-1", Number: 7}
provider := &fakeGitHubChangeLinks{
prs: []*github.TaskPR{{ID: "old-link", TaskID: "task-1", RepositoryID: "repo-1", PRNumber: 7}},
linkErr: tc.linkErr,
}
coordinator := taskChangeLinkCoordinator{
tasks: taskSvc, github: provider, singleUserIdentity: taskChangeLinkIdentityResolver(newDisabledAuthService(t)),
}
next := mcphandlers.TaskChangeLink{Provider: "github", RepositoryID: "repo-1", Number: 42}
if tc.same {
next = old
}
links, err := coordinator.ReplaceTaskChange(context.Background(), mcphandlers.TaskChangeLinkRequest{
TaskID: "task-1", Old: &old, Link: next,
})
switch {
case tc.linkErr != nil:
require.ErrorIs(t, err, tc.linkErr)
require.Empty(t, provider.unlinked)
require.Len(t, provider.prs, 1)
require.Equal(t, "old-link", provider.prs[0].ID)
case tc.same:
require.NoError(t, err)
require.Equal(t, []mcphandlers.TaskChangeLink{old}, links)
require.Empty(t, provider.linkedURL)
require.Empty(t, provider.unlinked)
default:
require.NoError(t, err)
require.Equal(t, []mcphandlers.TaskChangeLink{next}, links)
require.Equal(t, []string{"old-link"}, provider.unlinked)
require.Equal(t, userstore.DefaultUserID, provider.linkedUserID)
}
})
}
}