diff --git a/internal/analytics/collector_test.go b/internal/analytics/collector_test.go index 76ec0b71..e7958f2c 100644 --- a/internal/analytics/collector_test.go +++ b/internal/analytics/collector_test.go @@ -7,10 +7,8 @@ import ( "testing" "time" - "github.com/LeanerCloud/cloud-commitments-go/pkg/common" - "github.com/LeanerCloud/cloud-commitments-go/pkg/ladder" "github.com/LeanerCloud/cloud-commitments-platform/internal/config" - "github.com/jackc/pgx/v5" + "github.com/LeanerCloud/cloud-commitments-platform/internal/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -119,135 +117,13 @@ func (m *mockAnalyticsStore) Close() error { return nil } -// mockConfigStore implements config.StoreInterface for testing. +// mockConfigStore is the shared mocks.MockConfigStore with GetActivePurchaseHistory +// made injectable: that is the only config-store read the collector makes. Every +// other config.StoreInterface method is promoted from the shared mock, so a new +// store method needs a stub in internal/mocks only, not here as well (issue #76). type mockConfigStore struct { - getPurchaseHistoryFunc func(ctx context.Context, accountID string, limit int) ([]config.PurchaseHistoryRecord, error) - getAllPurchaseHistoryFunc func(ctx context.Context, limit int) ([]config.PurchaseHistoryRecord, error) - getActivePurchaseHistoryFunc func(ctx context.Context, asOf time.Time, accountIDs []string, externalIDsByProvider map[string][]string) ([]config.PurchaseHistoryRecord, error) - getPurchaseHistoryByPurchaseIDFunc func(ctx context.Context, purchaseID string) (*config.PurchaseHistoryRecord, error) - markPurchaseRevokedFunc func(ctx context.Context, purchaseID string, revokedAt time.Time, revokedVia string, supportCaseID string) error -} - -func (m *mockConfigStore) GetGlobalConfig(ctx context.Context) (*config.GlobalConfig, error) { - return nil, nil -} - -func (m *mockConfigStore) SaveGlobalConfig(ctx context.Context, cfg *config.GlobalConfig) error { - return nil -} - -func (m *mockConfigStore) GetServiceConfig(ctx context.Context, provider, service string) (*config.ServiceConfig, error) { - return nil, nil -} - -func (m *mockConfigStore) SaveServiceConfig(ctx context.Context, cfg *config.ServiceConfig) error { - return nil -} - -func (m *mockConfigStore) ListServiceConfigs(ctx context.Context) ([]config.ServiceConfig, error) { - return nil, nil -} - -func (m *mockConfigStore) CreatePurchasePlan(ctx context.Context, plan *config.PurchasePlan) error { - return nil -} - -func (m *mockConfigStore) GetPurchasePlan(ctx context.Context, planID string) (*config.PurchasePlan, error) { - return nil, nil -} - -func (m *mockConfigStore) UpdatePurchasePlan(ctx context.Context, plan *config.PurchasePlan) error { - return nil -} - -func (m *mockConfigStore) StampPlanNotificationSent(context.Context, string, time.Time) error { - return nil -} - -func (m *mockConfigStore) CompletePlanStep(ctx context.Context, planID string, stepNumber int) error { - return nil -} - -func (m *mockConfigStore) GetStuckRampSteps(_ context.Context) (map[string]config.RampStepBlock, error) { - return nil, nil -} - -func (m *mockConfigStore) LockPlanProvidersTx(_ context.Context, _ pgx.Tx, _ []string) error { - return nil -} - -func (m *mockConfigStore) LockPurchasePlanTx(_ context.Context, _ pgx.Tx, _ string) (*config.PurchasePlan, error) { - return nil, nil -} - -func (m *mockConfigStore) OccupiedRampStepsInRangeTx(_ context.Context, _ pgx.Tx, _ string, _, _ int) ([]int, error) { - return nil, nil -} - -func (m *mockConfigStore) UpdatePurchasePlanTx(ctx context.Context, _ pgx.Tx, plan *config.PurchasePlan) error { - return nil -} - -func (m *mockConfigStore) DeletePurchasePlan(ctx context.Context, planID string) error { - return nil -} - -func (m *mockConfigStore) ListPurchasePlans(ctx context.Context, filter config.PurchasePlanFilter) ([]config.PurchasePlan, error) { - return nil, nil -} - -func (m *mockConfigStore) SavePurchaseExecution(ctx context.Context, execution *config.PurchaseExecution) error { - return nil -} - -func (m *mockConfigStore) GetPendingExecutions(ctx context.Context) ([]config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) GetExecutionsByStatuses(ctx context.Context, statuses []string, limit int) ([]config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) CountExecutionsByPlanAndStatus(ctx context.Context, statuses []string, since time.Time) (map[string]config.ExecutionStatusCounts, error) { - return nil, nil -} - -func (m *mockConfigStore) GetPlannedExecutions(ctx context.Context, statuses []string, limit int) ([]config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) GetStaleApprovedExecutions(ctx context.Context, olderThan time.Duration) ([]config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) GetExecutionByID(ctx context.Context, executionID string) (*config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) GetExecutionByPlanAndDate(ctx context.Context, planID string, scheduledDate time.Time) (*config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) GetUserEmailByID(ctx context.Context, userID string) (string, error) { - return "", nil -} - -func (m *mockConfigStore) SavePurchaseHistory(ctx context.Context, record *config.PurchaseHistoryRecord) error { - return nil -} - -func (m *mockConfigStore) GetPurchaseHistory(ctx context.Context, accountID string, limit int) ([]config.PurchaseHistoryRecord, error) { - if m.getPurchaseHistoryFunc != nil { - return m.getPurchaseHistoryFunc(ctx, accountID, limit) - } - return nil, nil -} - -func (m *mockConfigStore) GetAllPurchaseHistory(ctx context.Context, limit int) ([]config.PurchaseHistoryRecord, error) { - if m.getAllPurchaseHistoryFunc != nil { - return m.getAllPurchaseHistoryFunc(ctx, limit) - } - return nil, nil + mocks.MockConfigStore + getActivePurchaseHistoryFunc func(ctx context.Context, asOf time.Time, accountIDs []string, externalIDsByProvider map[string][]string) ([]config.PurchaseHistoryRecord, error) } func (m *mockConfigStore) GetActivePurchaseHistory(ctx context.Context, asOf time.Time, accountIDs []string, externalIDsByProvider map[string][]string) ([]config.PurchaseHistoryRecord, error) { @@ -257,293 +133,6 @@ func (m *mockConfigStore) GetActivePurchaseHistory(ctx context.Context, asOf tim return nil, nil } -func (m *mockConfigStore) GetPurchaseHistoryFiltered(ctx context.Context, filter config.PurchaseHistoryFilter) ([]config.PurchaseHistoryRecord, error) { - return nil, nil -} - -func (m *mockConfigStore) GetPurchaseHistoryByPurchaseID(ctx context.Context, purchaseID string) (*config.PurchaseHistoryRecord, error) { - if m.getPurchaseHistoryByPurchaseIDFunc != nil { - return m.getPurchaseHistoryByPurchaseIDFunc(ctx, purchaseID) - } - return nil, nil -} - -func (m *mockConfigStore) MarkPurchaseRevoked(ctx context.Context, purchaseID string, revokedAt time.Time, revokedVia string, supportCaseID string, _ *float64, _ string) error { - if m.markPurchaseRevokedFunc != nil { - return m.markPurchaseRevokedFunc(ctx, purchaseID, revokedAt, revokedVia, supportCaseID) - } - return nil -} - -func (m *mockConfigStore) FlipPurchaseRevocationInFlight(_ context.Context, _ string) error { - return nil -} - -func (m *mockConfigStore) ClearRevocationInFlight(_ context.Context, _ string) error { - return nil -} - -func (m *mockConfigStore) GetPurchaseHistoryInFlight(_ context.Context) ([]*config.PurchaseHistoryRecord, error) { - return nil, nil -} - -func (m *mockConfigStore) GetScheduledExecutionsDue(_ context.Context) ([]config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) CleanupOldExecutions(ctx context.Context, retentionDays int) (int64, error) { - return 0, nil -} - -func (m *mockConfigStore) CountPendingExecutionsForAccount(ctx context.Context, accountID string) (int, error) { - return 0, nil -} - -func (m *mockConfigStore) CountLiveExecutionsForAccount(ctx context.Context, accountID string) (int, error) { - return 0, nil -} - -func (m *mockConfigStore) ListPendingExecutionIDsForAccount(ctx context.Context, accountID string) ([]string, error) { - return nil, nil -} - -func (m *mockConfigStore) TransitionExecutionStatus(ctx context.Context, executionID string, fromStatuses []string, toStatus string, actor *string) (*config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) TransitionExecutionStatusPinned(ctx context.Context, executionID string, fromStatuses []string, toStatus string, actor *string, sawEmptyRecommendations bool) (*config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) TransitionExecutionStatusPinnedTx(ctx context.Context, tx pgx.Tx, executionID string, fromStatuses []string, toStatus string, actor *string, sawEmptyRecommendations bool) (*config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) TransitionExecutionStatusTokenPinned(ctx context.Context, executionID string, fromStatuses []string, toStatus string, actor *string, rawToken string) (*config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) RescheduleAwaitingExecution(_ context.Context, _ string, _ time.Time) (bool, error) { - return false, nil -} - -func (m *mockConfigStore) SetCancelledBy(_ context.Context, _ string, _ string) error { - return nil -} - -func (m *mockConfigStore) RotatePendingApprovalToken(ctx context.Context, executionID, tokenHash string, expiresAt time.Time) (bool, error) { - return false, nil -} - -func (m *mockConfigStore) LinkRetryExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID, retryExecutionID string) (bool, error) { - return false, nil -} - -func (m *mockConfigStore) LockLadderTrancheForRetryTx(ctx context.Context, tx pgx.Tx, idempotencyKey string) (ladder.TrancheStatus, bool, error) { - return "", false, nil -} - -func (m *mockConfigStore) GetLadderExecutionGate(ctx context.Context, idempotencyKey string) (*config.LadderExecutionGate, error) { - return nil, nil -} - -func (m *mockConfigStore) CancelExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (bool, string, error) { - return false, "", nil -} - -func (m *mockConfigStore) CancelScheduledExecutionAtomic(ctx context.Context, tx pgx.Tx, executionID string, cancelledBy *string) (bool, string, error) { - return false, "", nil -} - -func (m *mockConfigStore) ListStuckExecutions(ctx context.Context, statuses []string, olderThan time.Duration) ([]config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) SaveRIExchangeRecord(ctx context.Context, record *config.RIExchangeRecord) error { - return nil -} -func (m *mockConfigStore) ReserveRIExchange(ctx context.Context, record *config.RIExchangeRecord, dailyCapUSD, perExchangeCapUSD string) (string, error) { - return "", errors.New("RI exchange reservation not configured in analytics test store") -} - -func (m *mockConfigStore) ReserveApprovedRIExchange(ctx context.Context, id, dailyCapUSD, perExchangeCapUSD string) (string, error) { - return "", errors.New("approved RI exchange reservation not configured in test store") -} -func (m *mockConfigStore) GetRIExchangeRecord(ctx context.Context, id string) (*config.RIExchangeRecord, error) { - return nil, nil -} -func (m *mockConfigStore) GetRIExchangeRecordByToken(ctx context.Context, token string) (*config.RIExchangeRecord, error) { - return nil, nil -} -func (m *mockConfigStore) GetRIExchangeHistory(ctx context.Context, since time.Time, limit int) ([]config.RIExchangeRecord, error) { - return nil, nil -} -func (m *mockConfigStore) GetRIExchangeHistoryForAccounts(ctx context.Context, since time.Time, limit int, accountIDs []string) ([]config.RIExchangeRecord, error) { - return nil, nil -} -func (m *mockConfigStore) TransitionRIExchangeStatus(ctx context.Context, id string, fromStatus string, toStatus string, actor *string) (*config.RIExchangeRecord, error) { - return nil, nil -} -func (m *mockConfigStore) CompleteRIExchange(ctx context.Context, id string, exchangeID string) error { - return nil -} -func (m *mockConfigStore) CompleteRIExchangeWithPayment(_ context.Context, _, _, _ string) error { - return nil -} -func (m *mockConfigStore) FailRIExchange(ctx context.Context, id string, errorMsg string) error { - return nil -} -func (m *mockConfigStore) StampRIExchangeApprovedBy(ctx context.Context, id string, approverEmail string) error { - return nil -} -func (m *mockConfigStore) GetRIExchangeDailySpend(ctx context.Context, date time.Time) (string, error) { - return "0", nil -} -func (m *mockConfigStore) CancelAllPendingExchanges(ctx context.Context) (int64, error) { - return 0, nil -} - -func (m *mockConfigStore) CancelPendingExchangesByOrigin(_ context.Context, _ common.ExchangeOrigin) (int64, error) { - return 0, nil -} -func (m *mockConfigStore) GetStaleProcessingExchanges(ctx context.Context, olderThan time.Duration) ([]config.RIExchangeRecord, error) { - return nil, nil -} - -func (m *mockConfigStore) SaveRIExchangeDirectExecution(_ context.Context, _ *config.RIExchangeDirectExecution) error { - return nil -} - -func (m *mockConfigStore) FinalizeRIExchangeDirectExecution(_ context.Context, _ string, _ config.RIExchangeDirectOutcome) error { - return nil -} - -func (m *mockConfigStore) CreateCloudAccount(ctx context.Context, account *config.CloudAccount) error { - return nil -} -func (m *mockConfigStore) GetCloudAccount(ctx context.Context, id string) (*config.CloudAccount, error) { - return nil, nil -} -func (m *mockConfigStore) GetCloudAccountByExternalID(ctx context.Context, provider, externalID string) (*config.CloudAccount, error) { - return nil, nil -} -func (m *mockConfigStore) UpdateCloudAccount(ctx context.Context, account *config.CloudAccount) error { - return nil -} -func (m *mockConfigStore) DeleteCloudAccount(ctx context.Context, id string) error { - return nil -} -func (m *mockConfigStore) ListCloudAccounts(ctx context.Context, filter config.CloudAccountFilter) ([]config.CloudAccount, error) { - return nil, nil -} -func (m *mockConfigStore) SaveAccountCredential(ctx context.Context, accountID, credentialType, encryptedBlob string) error { - return nil -} -func (m *mockConfigStore) GetAccountCredential(ctx context.Context, accountID, credentialType string) (string, error) { - return "", nil -} -func (m *mockConfigStore) DeleteAccountCredentials(ctx context.Context, accountID string) error { - return nil -} -func (m *mockConfigStore) HasAccountCredentials(ctx context.Context, accountID string) (bool, error) { - return false, nil -} -func (m *mockConfigStore) GetAccountServiceOverride(ctx context.Context, accountID, provider, service string) (*config.AccountServiceOverride, error) { - return nil, nil -} -func (m *mockConfigStore) SaveAccountServiceOverride(ctx context.Context, override *config.AccountServiceOverride) error { - return nil -} -func (m *mockConfigStore) DeleteAccountServiceOverride(ctx context.Context, accountID, provider, service string) error { - return nil -} -func (m *mockConfigStore) ListAccountServiceOverrides(ctx context.Context, accountID string) ([]config.AccountServiceOverride, error) { - return nil, nil -} -func (m *mockConfigStore) SetPlanAccounts(ctx context.Context, planID string, accountIDs []string) error { - return nil -} -func (m *mockConfigStore) GetPlanAccounts(ctx context.Context, planID string) ([]config.CloudAccount, error) { - return nil, nil -} -func (m *mockConfigStore) CreateAccountRegistration(_ context.Context, _ *config.AccountRegistration) error { - return nil -} -func (m *mockConfigStore) GetAccountRegistration(_ context.Context, _ string) (*config.AccountRegistration, error) { - return nil, nil -} -func (m *mockConfigStore) GetAccountRegistrationByToken(_ context.Context, _ string) (*config.AccountRegistration, error) { - return nil, nil -} -func (m *mockConfigStore) ListAccountRegistrations(_ context.Context, _ config.AccountRegistrationFilter) ([]config.AccountRegistration, error) { - return nil, nil -} -func (m *mockConfigStore) UpdateAccountRegistration(_ context.Context, _ *config.AccountRegistration) error { - return nil -} -func (m *mockConfigStore) TransitionRegistrationStatus(_ context.Context, _ *config.AccountRegistration, _ string, _ *string) error { - return nil -} -func (m *mockConfigStore) DeleteAccountRegistration(_ context.Context, _ string) error { - return nil -} -func (m *mockConfigStore) ReplaceRecommendations(_ context.Context, _ time.Time, _ []config.RecommendationRecord) error { - return nil -} -func (m *mockConfigStore) UpsertRecommendations(_ context.Context, _ time.Time, _ []config.RecommendationRecord, _ []config.SuccessfulCollect) error { - return nil -} -func (m *mockConfigStore) ListStoredRecommendations(_ context.Context, _ config.RecommendationFilter) ([]config.RecommendationRecord, error) { - return nil, nil -} -func (m *mockConfigStore) GetRecommendationsFreshness(_ context.Context) (*config.RecommendationsFreshness, error) { - return &config.RecommendationsFreshness{}, nil -} -func (m *mockConfigStore) SetRecommendationsCollectionError(_ context.Context, _ string) error { - return nil -} -func (m *mockConfigStore) SetRecommendationsCollectionPartial(_ context.Context, _ *config.RecommendationsCollectionPartial) error { - return nil -} -func (m *mockConfigStore) MarkCollectionStarted(_ context.Context) (string, bool, error) { - return "mock-owner-token", true, nil -} -func (m *mockConfigStore) ClearCollectionStarted(_ context.Context, _ string) error { - return nil -} -func (m *mockConfigStore) GetRIUtilizationCache(_ context.Context, _ string, _ int) (*config.RIUtilizationCacheEntry, error) { - return nil, nil -} -func (m *mockConfigStore) UpsertRIUtilizationCache(_ context.Context, _ string, _ int, _ []byte, _ time.Time) error { - return nil -} -func (m *mockConfigStore) UpsertNotificationMute(_ context.Context, _, _, _ string) error { - return nil -} -func (m *mockConfigStore) IsNotificationMuted(_ context.Context, _, _ string) (bool, error) { - return false, nil -} - -func (m *mockConfigStore) UpdatePurchaseHistoryListing(_ context.Context, _, _, _ string) error { - return nil -} - -func (m *mockConfigStore) ClaimMarketplaceListingSlot(_ context.Context, _, _ string, _ []byte) (*config.MarketplaceListingClaim, error) { - return &config.MarketplaceListingClaim{}, nil -} - -func (m *mockConfigStore) ReleaseMarketplaceListingClaim(_ context.Context, _, _, _ string, _ bool) error { - return nil -} - -func (m *mockConfigStore) ClaimRIExchangeIdempotencyKey(_ context.Context, _ string, _ time.Duration) (bool, error) { - return true, nil -} - -func (m *mockConfigStore) StampOfferingClass(_ context.Context, _, _ string) error { - return nil -} - // strPtr is a test helper for *string fields. func strPtr(s string) *string { return &s } @@ -839,115 +428,3 @@ func TestConstants(t *testing.T) { assert.Equal(t, 12, MonthsPerYear) }) } - -// ── Purchase suppressions (Commit 2 of bulk-purchase-with-grace). -func (m *mockConfigStore) CreateSuppression(_ context.Context, _ *config.PurchaseSuppression) error { - return nil -} -func (m *mockConfigStore) CreateSuppressionTx(_ context.Context, _ pgx.Tx, _ *config.PurchaseSuppression) error { - return nil -} -func (m *mockConfigStore) DeleteSuppressionsByExecution(_ context.Context, _ string) error { - return nil -} -func (m *mockConfigStore) DeleteSuppressionsByExecutionTx(_ context.Context, _ pgx.Tx, _ string) error { - return nil -} -func (m *mockConfigStore) ListActiveSuppressions(_ context.Context) ([]config.PurchaseSuppression, error) { - return nil, nil -} -func (m *mockConfigStore) SavePurchaseExecutionTx(ctx context.Context, _ pgx.Tx, e *config.PurchaseExecution) error { - return m.SavePurchaseExecution(ctx, e) -} -func (m *mockConfigStore) SaveExecutionIfStatus(ctx context.Context, e *config.PurchaseExecution, _ []string) error { - return m.SavePurchaseExecution(ctx, e) -} -func (m *mockConfigStore) SaveExecutionIfStatusTx(ctx context.Context, _ pgx.Tx, e *config.PurchaseExecution, _ []string) error { - return m.SavePurchaseExecution(ctx, e) -} -func (m *mockConfigStore) GetExecutionFanOutShape(_ context.Context, _ string) (string, error) { - return "", nil -} -func (m *mockConfigStore) PinExecutionFanOutShape(_ context.Context, _ string, shape string) (string, error) { - return shape, nil -} -func (m *mockConfigStore) RecoverReapedExecution(ctx context.Context, e *config.PurchaseExecution) error { - return m.SavePurchaseExecution(ctx, e) -} -func (m *mockConfigStore) RecoverReapedExecutionTx(ctx context.Context, _ pgx.Tx, e *config.PurchaseExecution) error { - return m.SavePurchaseExecution(ctx, e) -} -func (m *mockConfigStore) ListRecentSubmitsTx(ctx context.Context, _ pgx.Tx, _ string, _ time.Time) ([]config.PurchaseExecution, error) { - return m.GetPendingExecutions(ctx) -} -func (m *mockConfigStore) GetPendingExecutionsTx(ctx context.Context, _ pgx.Tx) ([]config.PurchaseExecution, error) { - return m.GetPendingExecutions(ctx) -} -func (m *mockConfigStore) WithTx(_ context.Context, fn func(tx pgx.Tx) error) error { return fn(nil) } - -func (m *mockConfigStore) GetLadderConfigs(_ context.Context) ([]config.LadderConfigDB, error) { - return nil, nil -} -func (m *mockConfigStore) GetLadderConfig(_ context.Context, _, _ string) (*config.LadderConfigDB, error) { - return nil, nil -} -func (m *mockConfigStore) UpsertLadderConfig(_ context.Context, cfg *config.LadderConfigDB) (*config.LadderConfigDB, error) { - return cfg, nil -} -func (m *mockConfigStore) SaveLadderRun(_ context.Context, run *config.LadderRunDB) (*config.LadderRunDB, error) { - return run, nil -} -func (m *mockConfigStore) PlanAndSaveLadderRun(_ context.Context, _ string, _ time.Time, build config.LadderPlanBuilder) (*config.LadderRunDB, error) { - run, _, err := build(nil) - return run, err -} -func (m *mockConfigStore) GetLadderRun(_ context.Context, _ string) (*config.LadderRunDB, error) { - return nil, nil -} -func (m *mockConfigStore) SaveLadderTranches(_ context.Context, _ []config.LadderTrancheDB) error { - return nil -} -func (m *mockConfigStore) LatestLadderRunStartedAt(_ context.Context, _ string) (*time.Time, error) { - return nil, nil -} -func (m *mockConfigStore) TransitionLadderRunStatus(_ context.Context, _ string, _ []ladder.RunStatus, _ ladder.RunStatus) (*config.LadderRunDB, error) { - return nil, nil -} -func (m *mockConfigStore) CancelLadderRun(_ context.Context, _ string, _ []ladder.RunStatus, _ string) (*config.LadderRunDB, error) { - return nil, nil -} -func (m *mockConfigStore) UpdateGlobalConfigAtomic(_ context.Context, apply func(*config.GlobalConfig) error) (*config.GlobalConfig, error) { - cfg := &config.GlobalConfig{} - if err := apply(cfg); err != nil { - return nil, err - } - return cfg, nil -} - -func (m *mockConfigStore) ListExecutionsForPlanStepTx(_ context.Context, _ pgx.Tx, _ string, _ int) ([]config.PurchaseExecution, error) { - return nil, nil -} - -func (m *mockConfigStore) SetPlanRampBaselineTx(_ context.Context, _ pgx.Tx, _ string, _ config.PlanRampBaseline) (bool, error) { - return false, nil -} - -func (m *mockConfigStore) GetPlanRampBaselineTx(_ context.Context, _ pgx.Tx, _ string) (config.PlanRampBaseline, error) { - return nil, nil -} - -func (m *mockConfigStore) ListPlanCommittedTx(_ context.Context, _ pgx.Tx, _ string) (map[string]config.PlanRampQuantity, error) { - return nil, nil -} - -func (m *mockConfigStore) CompleteTargetMetStepTx(_ context.Context, _ pgx.Tx, _ string, _ int, _, _ string) error { - return nil -} - -func (m *mockConfigStore) SetExecutionRecommendationsIfEmptyTx(_ context.Context, _ pgx.Tx, _ string, _ []config.RecommendationRecord, _, _ float64) (bool, error) { - return false, nil -} - -func (m *mockConfigStore) ExpireExecutionAtomic(_ context.Context, _ pgx.Tx, _ string) (bool, error) { - return false, nil -} diff --git a/internal/mocks/config_store_copies_guard_test.go b/internal/mocks/config_store_copies_guard_test.go new file mode 100644 index 00000000..a5a83ff2 --- /dev/null +++ b/internal/mocks/config_store_copies_guard_test.go @@ -0,0 +1,125 @@ +package mocks + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strings" + "testing" +) + +// handRolledConfigStoreAllowlist lists the hand-rolled config.StoreInterface +// copies that predate this guard and are still waiting to be folded onto +// MockConfigStore (issue #76). It may only shrink: an entry whose type no +// longer hand-rolls the interface fails the guard until it is removed here. +var handRolledConfigStoreAllowlist = map[string]bool{ + "internal/server.mockConfigStoreForHealth": true, +} + +// TestNoHandRolledConfigStoreMocks keeps test doubles of config.StoreInterface +// from being written out method by method. Every such copy needs a new stub for +// every new store method, which is the per-method tax issue #76 describes. A +// test double should embed MockConfigStore (or the interface) and override only +// the methods its test exercises. +// +// The check is syntactic: a type declared in a _test.go file that defines more +// than half of StoreInterface's methods is a hand-rolled copy. Partial +// overrides on top of an embedded store define a handful. +func TestNoHandRolledConfigStoreMocks(t *testing.T) { + root := repoRoot(t) + ifaceMethods := storeInterfaceMethods(t, filepath.Join(root, "internal", "config", "interfaces.go")) + if len(ifaceMethods) < 50 { + t.Fatalf("parsed only %d StoreInterface methods; the interface lookup has broken", len(ifaceMethods)) + } + + defined := map[string]map[string]bool{} // "." -> StoreInterface methods it defines + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + switch d.Name() { + case ".git", "node_modules", "vendor", ".terraform", "frontend": + return filepath.SkipDir + } + return nil + } + if !strings.HasSuffix(path, "_test.go") { + return nil + } + f, perr := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution) + if perr != nil { + return nil // a file that does not parse fails its own package's build + } + rel, _ := filepath.Rel(root, filepath.Dir(path)) + for _, decl := range f.Decls { + fd, ok := decl.(*ast.FuncDecl) + if !ok || fd.Recv == nil || len(fd.Recv.List) != 1 || !ifaceMethods[fd.Name.Name] { + continue + } + typeName := recvTypeIdent(fd.Recv.List[0].Type) + if typeName == "" { + continue + } + key := filepath.ToSlash(rel) + "." + typeName + if defined[key] == nil { + defined[key] = map[string]bool{} + } + defined[key][fd.Name.Name] = true + } + return nil + }) + if err != nil { + t.Fatalf("walk %s: %v", root, err) + } + + var copies []string + for key, methods := range defined { + handRolled := len(methods)*2 > len(ifaceMethods) + switch { + case handRolled && !handRolledConfigStoreAllowlist[key]: + copies = append(copies, key) + case !handRolled && handRolledConfigStoreAllowlist[key]: + t.Errorf("%s no longer hand-rolls config.StoreInterface; remove it from handRolledConfigStoreAllowlist", key) + } + } + for key := range handRolledConfigStoreAllowlist { + if defined[key] == nil { + t.Errorf("%s is allowlisted but defines no StoreInterface method; remove it from handRolledConfigStoreAllowlist", key) + } + } + if len(copies) > 0 { + sort.Strings(copies) + t.Errorf("hand-rolled config.StoreInterface test doubles (embed mocks.MockConfigStore and override only "+ + "what the test needs instead):\n %s", strings.Join(copies, "\n ")) + } +} + +// storeInterfaceMethods returns the method names declared on +// config.StoreInterface in the given file. +func storeInterfaceMethods(t *testing.T, path string) map[string]bool { + t.Helper() + f, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + methods := map[string]bool{} + ast.Inspect(f, func(n ast.Node) bool { + ts, ok := n.(*ast.TypeSpec) + if !ok || ts.Name.Name != "StoreInterface" { + return true + } + if it, ok := ts.Type.(*ast.InterfaceType); ok { + for _, field := range it.Methods.List { + for _, name := range field.Names { + methods[name.Name] = true + } + } + } + return false + }) + return methods +}