ipa2korg / backend /internal /application /account /linked_delete_test.go
FUCAT
Update to v3.0.11 with HF deployment fixes
cdc337a
Raw
History Blame Contribute Delete
10.3 kB
package account
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
"time"
accountdomain "github.com/chenyme/grok2api/backend/internal/domain/account"
"github.com/chenyme/grok2api/backend/internal/infra/persistence/relational"
"github.com/chenyme/grok2api/backend/internal/repository"
)
func TestBatchDeleteWithLinkedRemovesPeersAndKeepsUntargetedWeb(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, service := newLinkedDeleteTestService(t, "svc-linked-delete.db")
web, build, console := seedLinkedTrio(t, repo, strings.Repeat("1", 64), "u1")
result, err := service.BatchDeleteWithLinked(ctx, accountdomain.ProviderBuild, []uint64{build.ID}, []accountdomain.Provider{accountdomain.ProviderConsole})
if err != nil {
t.Fatal(err)
}
if result.Deleted != 2 || result.LinkedDeleted != 1 {
t.Fatalf("result = %#v", result)
}
assertAccountMissing(t, repo, build.ID)
assertAccountMissing(t, repo, console.ID)
assertAccountPresent(t, repo, web.ID)
web2 := mustUpsertLinked(t, repo, accountdomain.Credential{
Provider: accountdomain.ProviderWeb, AuthType: accountdomain.AuthTypeSSO, Name: "web2", SourceKey: "sso:" + strings.Repeat("2", 64),
})
deleted, err := service.BatchDelete(ctx, []uint64{web2.ID})
if err != nil || deleted != 1 {
t.Fatalf("legacy batch delete deleted=%d err=%v", deleted, err)
}
}
func TestBatchDeleteWithLinkedWebDeletesBuildAndConsole(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, service := newLinkedDeleteTestService(t, "svc-linked-delete-web.db")
web, build, console := seedLinkedTrio(t, repo, strings.Repeat("a", 64), "u-web")
result, err := service.BatchDeleteWithLinked(ctx, accountdomain.ProviderWeb, []uint64{web.ID}, []accountdomain.Provider{accountdomain.ProviderBuild, accountdomain.ProviderConsole})
if err != nil {
t.Fatal(err)
}
if result.Deleted != 3 || result.RootsDeleted != 1 || result.LinkedDeleted != 2 {
t.Fatalf("result = %#v", result)
}
assertAccountMissing(t, repo, web.ID)
assertAccountMissing(t, repo, build.ID)
assertAccountMissing(t, repo, console.ID)
}
func TestBatchDeleteWithLinkedMixedBatchOnlyExpandsLinkedRoots(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, service := newLinkedDeleteTestService(t, "svc-linked-delete-mix.db")
webLinked, _, console := seedLinkedTrio(t, repo, strings.Repeat("b", 64), "u-mix")
webOnly := mustUpsertLinked(t, repo, accountdomain.Credential{
Provider: accountdomain.ProviderWeb, AuthType: accountdomain.AuthTypeSSO, Name: "web-only", SourceKey: "sso:" + strings.Repeat("c", 64),
})
result, err := service.BatchDeleteWithLinked(ctx, accountdomain.ProviderWeb, []uint64{webLinked.ID, webOnly.ID}, []accountdomain.Provider{accountdomain.ProviderConsole})
if err != nil {
t.Fatal(err)
}
if result.Deleted != 3 {
t.Fatalf("result = %#v", result)
}
assertAccountMissing(t, repo, webLinked.ID)
assertAccountMissing(t, repo, webOnly.ID)
assertAccountMissing(t, repo, console.ID)
}
func TestPreviewLinkedDeleteCountsWithoutDeleting(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, service := newLinkedDeleteTestService(t, "svc-linked-preview.db")
web, build, console := seedLinkedTrio(t, repo, strings.Repeat("d", 64), "u-prev")
res, err := service.PreviewLinkedDelete(ctx, accountdomain.ProviderWeb, []uint64{web.ID}, []accountdomain.Provider{accountdomain.ProviderBuild, accountdomain.ProviderConsole})
if err != nil {
t.Fatal(err)
}
if len(res.RootIDs) != 1 || res.LinkedByProvider[accountdomain.ProviderBuild] != 1 || res.LinkedByProvider[accountdomain.ProviderConsole] != 1 || len(res.FinalIDs) != 3 {
t.Fatalf("preview = %#v", res)
}
assertAccountPresent(t, repo, web.ID)
assertAccountPresent(t, repo, build.ID)
assertAccountPresent(t, repo, console.ID)
}
func TestBatchDeleteWithLinkedEmptyTargetsDeletesRootOnly(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, service := newLinkedDeleteTestService(t, "svc-linked-empty-targets.db")
web, build, console := seedLinkedTrio(t, repo, strings.Repeat("e", 64), "u-empty")
result, err := service.BatchDeleteWithLinked(ctx, accountdomain.ProviderWeb, []uint64{web.ID}, nil)
if err != nil {
t.Fatal(err)
}
if result.Deleted != 1 || result.LinkedDeleted != 0 {
t.Fatalf("result = %#v", result)
}
assertAccountMissing(t, repo, web.ID)
assertAccountPresent(t, repo, build.ID)
assertAccountPresent(t, repo, console.ID)
}
func TestBatchDeleteWithLinkedInvalidTarget(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, service := newLinkedDeleteTestService(t, "svc-linked-bad-target.db")
web, _, _ := seedLinkedTrio(t, repo, strings.Repeat("f", 64), "u-bad")
if _, err := service.BatchDeleteWithLinked(ctx, accountdomain.ProviderWeb, []uint64{web.ID}, []accountdomain.Provider{accountdomain.Provider("nope")}); err == nil {
t.Fatal("expected invalid target error")
}
if _, err := service.BatchDeleteWithLinked(ctx, accountdomain.ProviderWeb, []uint64{web.ID}, []accountdomain.Provider{accountdomain.ProviderWeb}); err == nil {
t.Fatal("expected self-target error")
}
assertAccountPresent(t, repo, web.ID)
}
func TestDeleteMissingAccountReturnsNotFound(t *testing.T) {
t.Parallel()
ctx := context.Background()
_, service := newLinkedDeleteTestService(t, "svc-delete-missing.db")
if err := service.Delete(ctx, 9_999_999); !errors.Is(err, ErrNotFound) {
t.Fatalf("Delete missing: err=%v", err)
}
if _, err := service.DeleteWithLinked(ctx, accountdomain.ProviderWeb, 9_999_999, []accountdomain.Provider{accountdomain.ProviderBuild}); !errors.Is(err, ErrNotFound) {
t.Fatalf("DeleteWithLinked missing: err=%v", err)
}
}
func TestDeleteWithLinkedRejectsRootFromAnotherProvider(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo, service := newLinkedDeleteTestService(t, "svc-delete-provider-mismatch.db")
web, _, _ := seedLinkedTrio(t, repo, strings.Repeat("9", 64), "u-provider-mismatch")
_, err := service.DeleteWithLinked(ctx, accountdomain.ProviderBuild, web.ID, []accountdomain.Provider{accountdomain.ProviderConsole})
if !errors.Is(err, ErrConflict) {
t.Fatalf("provider mismatch error = %v", err)
}
assertAccountPresent(t, repo, web.ID)
}
func TestFinishLinkedDeleteUsesBatchStickyCleanup(t *testing.T) {
sticky := &stickyBatchStub{}
service := &Service{sticky: sticky}
service.finishLinkedDelete(context.Background(), []uint64{3, 5, 8})
if sticky.singleCalls != 0 {
t.Fatalf("single-account cleanup calls = %d", sticky.singleCalls)
}
if len(sticky.batchCalls) != 1 || len(sticky.batchCalls[0]) != 3 || sticky.batchCalls[0][0] != 3 || sticky.batchCalls[0][2] != 8 {
t.Fatalf("batch cleanup calls = %#v", sticky.batchCalls)
}
}
func newLinkedDeleteTestService(t *testing.T, dbName string) (*relational.AccountRepository, *Service) {
t.Helper()
ctx := context.Background()
database, err := relational.OpenSQLite(ctx, filepath.Join(t.TempDir(), dbName))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
if err := database.InitializeSchema(ctx); err != nil {
t.Fatal(err)
}
repo := relational.NewAccountRepository(database)
return repo, &Service{accounts: repo, sticky: stickyStub{}, logger: nil}
}
func seedLinkedTrio(t *testing.T, repo *relational.AccountRepository, digest, userID string) (web, build, console accountdomain.Credential) {
t.Helper()
ctx := context.Background()
web = mustUpsertLinked(t, repo, accountdomain.Credential{
Provider: accountdomain.ProviderWeb, AuthType: accountdomain.AuthTypeSSO, Name: "web", SourceKey: "sso:" + digest, UserID: userID,
})
build = mustUpsertLinked(t, repo, accountdomain.Credential{
Provider: accountdomain.ProviderBuild, AuthType: accountdomain.AuthTypeOAuth, Name: "build", SourceKey: "build-" + digest[:8], UserID: userID,
})
console = mustUpsertLinked(t, repo, accountdomain.Credential{
Provider: accountdomain.ProviderConsole, AuthType: accountdomain.AuthTypeSSO, Name: "console", SourceKey: "console-sso:" + digest, UserID: userID,
})
if err := repo.LinkWebToBuild(ctx, web.ID, build.ID); err != nil {
t.Fatal(err)
}
if err := repo.ReconcileProviderLinks(ctx, web.ID); err != nil {
t.Fatal(err)
}
return web, build, console
}
func mustUpsertLinked(t *testing.T, repo *relational.AccountRepository, value accountdomain.Credential) accountdomain.Credential {
t.Helper()
value.EncryptedAccessToken = "encrypted"
value.Enabled = true
value.AuthStatus = accountdomain.AuthStatusActive
value.Priority = accountdomain.DefaultPriority
value.MaxConcurrent = accountdomain.DefaultMaxConcurrent
stored, _, err := repo.UpsertByIdentity(context.Background(), value)
if err != nil {
t.Fatal(err)
}
return stored
}
func assertAccountMissing(t *testing.T, repo *relational.AccountRepository, id uint64) {
t.Helper()
if _, err := repo.Get(context.Background(), id); !errors.Is(err, repository.ErrNotFound) {
t.Fatalf("account %d should be missing, err=%v", id, err)
}
}
func assertAccountPresent(t *testing.T, repo *relational.AccountRepository, id uint64) {
t.Helper()
if _, err := repo.Get(context.Background(), id); err != nil {
t.Fatalf("account %d should remain: %v", id, err)
}
}
type stickyStub struct{}
func (stickyStub) Get(context.Context, string, time.Time) (uint64, bool, error) {
return 0, false, nil
}
func (stickyStub) Bind(context.Context, string, uint64, time.Time, time.Time) (uint64, error) {
return 0, nil
}
func (stickyStub) Set(context.Context, string, uint64, time.Time) error { return nil }
func (stickyStub) DeleteByAccount(context.Context, uint64) error { return nil }
type stickyBatchStub struct {
singleCalls int
batchCalls [][]uint64
}
func (*stickyBatchStub) Get(context.Context, string, time.Time) (uint64, bool, error) {
return 0, false, nil
}
func (*stickyBatchStub) Bind(context.Context, string, uint64, time.Time, time.Time) (uint64, error) {
return 0, nil
}
func (*stickyBatchStub) Set(context.Context, string, uint64, time.Time) error { return nil }
func (s *stickyBatchStub) DeleteByAccount(context.Context, uint64) error {
s.singleCalls++
return nil
}
func (s *stickyBatchStub) DeleteByAccounts(_ context.Context, ids []uint64) error {
s.batchCalls = append(s.batchCalls, append([]uint64(nil), ids...))
return nil
}