From f79368914c109bca1149d6c164ecef9b12612e71 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Thu, 29 Oct 2026 01:36:06 +0000 Subject: [PATCH] test(api): assert mock expectations in the first half of the api tests Every internal/api test in the files up to handler_config_test.go that registers a required .On(...) now fails when that call never happens. Each testify mock gets a t.Cleanup(AssertExpectations) where it is built, and the shared constructors assert their own mocks (scopedHandler, newScopeTestHandler, newAPIKeyRoundtripHandler, sharedNameHandler, analyticsParamsCall). Helpers that built mocks without a *testing.T (hostCheckHandler, setupRouterForDispatch, sessionApproveHandler, tokenApproveHandler) now take one. Expectations that never fired were stubs for a path the test does not take, not missed production calls: - shared fixtures stub calls a direct gate call, a list or an early refusal never reaches; relaxUnreachedStubs marks those optional per test, next to the AssertNotCalled/side-channel checks that already pin them - the analytics helper's per-endpoint query stubs are optional, and a served request must now run exactly its endpoint's queries and read the allow-list - direct execute no longer saves the row itself (DirectExecute stamps the audit fields after its claim), and approvePurchase refuses a sessionless caller with a 401 before the execution lookup: the stale stubs are removed and the 401 is asserted - the service-config PUT now checks that a refused save never saves and an accepted one saves once No production code changes. Refs #35 Co-Authored-By: Claude Opus 5.5 --- .../api/account_scope_fail_closed_test.go | 3 + .../api/account_scope_shared_name_test.go | 8 + .../analytics_account_ids_integration_test.go | 1 + .../api/apikey_constraints_roundtrip_test.go | 9 +- internal/api/apikey_explicit_zero_cap_test.go | 5 +- internal/api/authpublic_csrf_guard_test.go | 5 + internal/api/auto_purchase_grant_test.go | 33 ++-- internal/api/claimed_row_check_test.go | 12 ++ ...combined_plan_purchase_integration_test.go | 2 + internal/api/coverage_extras_test.go | 35 +++- internal/api/coverage_gaps_test.go | 14 ++ .../api/delete_planned_purchase_scope_test.go | 4 + .../api/executed_notification_flow_test.go | 11 +- .../api/executed_notification_partial_test.go | 22 ++- internal/api/execution_scope_test.go | 12 ++ .../four_eyes_unattributed_creator_test.go | 14 ++ .../api/global_config_arming_grants_test.go | 1 + internal/api/grantadmin_carveout_test.go | 1 + internal/api/grantscoped_test.go | 1 + internal/api/group_explicit_zero_cap_test.go | 2 + .../api/handler_accounts_awsprobe_test.go | 2 + .../api/handler_accounts_controlchar_test.go | 6 + .../api/handler_accounts_external_id_test.go | 18 ++- .../api/handler_accounts_hostcheck_test.go | 37 +++-- .../handler_accounts_partition_notice_test.go | 5 + ...andler_accounts_provider_immutable_test.go | 3 + internal/api/handler_accounts_router_test.go | 19 ++- internal/api/handler_accounts_test.go | 149 ++++++++++++++++++ .../api/handler_accounts_update_keep_test.go | 2 + .../handler_analytics_invalid_params_test.go | 6 + .../handler_analytics_scope_provider_test.go | 40 ++++- internal/api/handler_analytics_test.go | 46 ++++++ internal/api/handler_apikeys_test.go | 33 ++++ .../api/handler_auth_login_mfa_limit_test.go | 3 + .../handler_auth_me_permissions_scope_test.go | 2 + internal/api/handler_auth_test.go | 33 ++++ ...ler_config_propagation_integration_test.go | 2 + ...er_config_service_race_integration_test.go | 2 + internal/api/handler_config_stamps_test.go | 3 + internal/api/handler_config_test.go | 58 ++++++- 40 files changed, 607 insertions(+), 57 deletions(-) diff --git a/internal/api/account_scope_fail_closed_test.go b/internal/api/account_scope_fail_closed_test.go index 42cd51989..88444652d 100644 --- a/internal/api/account_scope_fail_closed_test.go +++ b/internal/api/account_scope_fail_closed_test.go @@ -61,6 +61,7 @@ func TestGetAllowedAccounts_PropagatesResolverFailure(t *testing.T) { func TestRequireAccountAccess_RefusesWhenScopeUnestablishable(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) m := new(MockAuthService) t.Cleanup(func() { m.AssertExpectations(t) }) @@ -128,6 +129,7 @@ func TestRequireAccountAccess_ScopedPrincipalUnchanged(t *testing.T) { t.Run("in-scope account is reachable", func(t *testing.T) { mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) m := new(MockAuthService) t.Cleanup(func() { m.AssertExpectations(t) }) mockStore.On("GetCloudAccount", ctx, scopeAcctA). @@ -142,6 +144,7 @@ func TestRequireAccountAccess_ScopedPrincipalUnchanged(t *testing.T) { t.Run("out-of-scope account is refused", func(t *testing.T) { mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) m := new(MockAuthService) t.Cleanup(func() { m.AssertExpectations(t) }) mockStore.On("GetCloudAccount", ctx, scopeAcctB). diff --git a/internal/api/account_scope_shared_name_test.go b/internal/api/account_scope_shared_name_test.go index 57316626d..89486b169 100644 --- a/internal/api/account_scope_shared_name_test.go +++ b/internal/api/account_scope_shared_name_test.go @@ -35,11 +35,13 @@ var ( func sharedNameHandler(t *testing.T, scope []string, accounts []config.CloudAccount, listErr error) *Handler { t.Helper() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, "tok").Return(&Session{UserID: "u-1", Email: "u@example.com"}, nil) mockAuth.On("HasPermissionAPI", mock.Anything, "u-1", mock.Anything, mock.Anything).Return(true, nil) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, "u-1").Return(scope, nil) store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.ListCloudAccountsFn = func(context.Context, config.CloudAccountFilter) ([]config.CloudAccount, error) { if listErr != nil { return nil, listErr @@ -225,6 +227,9 @@ func TestAccountScope_SharedDisplayName_OtherGates(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() h := sharedNameHandler(t, tc.scope, registered, nil) + // The gates are called directly with a session: no bearer token + // is validated and no route permission is checked. + relaxUnreachedStubs(&h.auth.(*MockAuthService).Mock, "ValidateSession", "HasPermissionAPI") store := h.config.(*MockConfigStore) store.GetPlanAccountsFn = func(context.Context, string) ([]config.CloudAccount, error) { return []config.CloudAccount{sharedNameAWS}, nil @@ -275,6 +280,9 @@ func TestAccountScope_EditorControlledExternalIDGrantsNothing(t *testing.T) { t.Parallel() // Listed in name order, as ListCloudAccounts returns them. h := sharedNameHandler(t, []string{tc.editor.ID}, []config.CloudAccount{shadowVictim, tc.editor}, nil) + // The gates are called directly with a session: no bearer token + // is validated and no route permission is checked. + relaxUnreachedStubs(&h.auth.(*MockAuthService).Mock, "ValidateSession", "HasPermissionAPI") target, own := tc.targetID, tc.editor.ID err := h.validatePurchaseRecommendationScope(ctx, session, []config.RecommendationRecord{{CloudAccountID: &target}}) diff --git a/internal/api/analytics_account_ids_integration_test.go b/internal/api/analytics_account_ids_integration_test.go index d8a446341..06fb5fe6b 100644 --- a/internal/api/analytics_account_ids_integration_test.go +++ b/internal/api/analytics_account_ids_integration_test.go @@ -50,6 +50,7 @@ func TestAnalyticsAccountIDs_SumsSelectedAccountsOnly_DB(t *testing.T) { newHandler := func(allowed []string) *Handler { auth := new(MockAuthService) + t.Cleanup(func() { auth.AssertExpectations(t) }) auth.On("ValidateSession", mock.Anything, "tok").Return(&Session{UserID: "u-1"}, nil) auth.On("HasPermissionAPI", mock.Anything, "u-1", mock.Anything, mock.Anything).Return(true, nil) auth.On("GetAllowedAccountsAPI", mock.Anything, "u-1").Return(allowed, nil) diff --git a/internal/api/apikey_constraints_roundtrip_test.go b/internal/api/apikey_constraints_roundtrip_test.go index 64d765712..bd6ac2904 100644 --- a/internal/api/apikey_constraints_roundtrip_test.go +++ b/internal/api/apikey_constraints_roundtrip_test.go @@ -51,6 +51,7 @@ func (a *realAPIKeyAuth) ListUserAPIKeysAPI(ctx context.Context, userID string) func newAPIKeyRoundtripHandler(t *testing.T) (*Handler, *auth.MockStore, **auth.UserAPIKey) { t.Helper() store := new(auth.MockStore) + t.Cleanup(func() { store.AssertExpectations(t) }) user := &auth.User{ ID: roundtripUserID, Email: "owner@example.com", @@ -70,6 +71,7 @@ func newAPIKeyRoundtripHandler(t *testing.T) (*Handler, *auth.MockStore, **auth. Return(nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, roundtripToken). Return(&Session{UserID: roundtripUserID, Email: user.Email}, nil) mockAuth.On("ValidateCSRFToken", mock.Anything, roundtripToken, roundtripCSRF).Return(nil).Maybe() @@ -161,6 +163,8 @@ func TestHandleRequest_CreateAPIKey_ResponseUsesDocumentedConstraintShape(t *tes func TestHandleRequest_ListAPIKeys_ReturnsDocumentedConstraintShape(t *testing.T) { h, store, _ := newAPIKeyRoundtripHandler(t) + // Listing creates nothing and does not read the owner's group. + relaxUnreachedStubs(&store.Mock, "GetGroup", "CreateAPIKey") // Seeded directly in the stored (auth) shape so this checks the listing // response independently of the create path. @@ -200,7 +204,10 @@ func TestHandleRequest_ListAPIKeys_ReturnsDocumentedConstraintShape(t *testing.T // level. Before #957 the negative max_amount was dropped during decoding and // the key was minted with no cap at all. func TestHandleRequest_CreateAPIKey_RejectsNegativeMaxAmount(t *testing.T) { - h, _, stored := newAPIKeyRoundtripHandler(t) + h, store, stored := newAPIKeyRoundtripHandler(t) + // The cap is refused before the owner's group is read or a key is + // stored; *stored below pins the latter. + relaxUnreachedStubs(&store.Mock, "GetGroup", "CreateAPIKey") resp := apiKeyRoundtripRequest(t, h, "POST", constrainedAPIKeyBody(t, -1)) diff --git a/internal/api/apikey_explicit_zero_cap_test.go b/internal/api/apikey_explicit_zero_cap_test.go index eb02da7dd..0035def37 100644 --- a/internal/api/apikey_explicit_zero_cap_test.go +++ b/internal/api/apikey_explicit_zero_cap_test.go @@ -30,7 +30,10 @@ func apiKeyBodyWithConstraints(constraints string) string { func TestHandleRequest_CreateAPIKey_RejectsExplicitZeroMaxAmount(t *testing.T) { for _, literal := range []string{`0`, `0.0`, `-0`, `0e5`} { t.Run(literal, func(t *testing.T) { - h, _, stored := newAPIKeyRoundtripHandler(t) + h, store, stored := newAPIKeyRoundtripHandler(t) + // The handler refuses the cap before the owner is read or a key + // is stored; *stored below pins the latter. + relaxUnreachedStubs(&store.Mock, "GetUserByID", "GetGroup", "CreateAPIKey") resp := apiKeyRoundtripRequest(t, h, "POST", apiKeyBodyWithConstraints(`{"regions":["us-east-1"],"max_amount":`+literal+`}`)) diff --git a/internal/api/authpublic_csrf_guard_test.go b/internal/api/authpublic_csrf_guard_test.go index e1ffadfcf..95b9754e7 100644 --- a/internal/api/authpublic_csrf_guard_test.go +++ b/internal/api/authpublic_csrf_guard_test.go @@ -257,6 +257,7 @@ func purchaseCSRFGuardFixture(action string, viaToken bool) func(t *testing.T) c } store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, id string) (*config.CloudAccount, error) { return &config.CloudAccount{ID: id, Name: id, Provider: "aws", ContactEmail: contact}, nil } @@ -266,6 +267,7 @@ func purchaseCSRFGuardFixture(action string, viaToken bool) func(t *testing.T) c store.On("SetCancelledBy", mock.Anything, csrfGuardExecID, mock.Anything).Return(nil).Maybe() pm := new(MockPurchaseManager) + t.Cleanup(func() { pm.AssertExpectations(t) }) pm.On("ApproveAndExecute", mock.Anything, csrfGuardExecID, mock.Anything, mock.Anything).Return(exec, "", nil).Maybe() pm.On("ApproveExecution", mock.Anything, csrfGuardExecID, csrfGuardRawToken, mock.Anything).Return(nil, "", nil).Maybe() pm.On("CancelExecution", mock.Anything, csrfGuardExecID, csrfGuardRawToken, mock.Anything).Return(nil).Maybe() @@ -278,6 +280,7 @@ func purchaseCSRFGuardFixture(action string, viaToken bool) func(t *testing.T) c m = csrfGuardAuth("admin@example.com") m.grantAdminPurchaser() } + t.Cleanup(func() { m.AssertExpectations(t) }) refused := func(t *testing.T) { t.Helper() @@ -316,6 +319,7 @@ func riApproveCSRFGuardFixture(t *testing.T) csrfGuardFixture { t.Helper() creatorID := "creator-uuid" store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetRIExchangeRecord", mock.Anything, csrfGuardRIID).Return(&config.RIExchangeRecord{ ID: csrfGuardRIID, Status: "pending", @@ -335,6 +339,7 @@ func riApproveCSRFGuardFixture(t *testing.T) csrfGuardFixture { store.On("StampRIExchangeApprovedBy", mock.Anything, csrfGuardRIID, mock.Anything).Return(nil).Maybe() m := csrfGuardAuth("admin@example.com") + t.Cleanup(func() { m.AssertExpectations(t) }) m.grantAdminPurchaser() refused := func(t *testing.T) { diff --git a/internal/api/auto_purchase_grant_test.go b/internal/api/auto_purchase_grant_test.go index fb200f23b..78eaff56e 100644 --- a/internal/api/auto_purchase_grant_test.go +++ b/internal/api/auto_purchase_grant_test.go @@ -119,6 +119,7 @@ func TestAutoPurchaseGate_PlanWrites(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetPurchasePlan", mock.Anything, apgPlanID).Return(tc.stored, nil) store.On("UpdatePurchasePlan", mock.Anything, mock.Anything).Return(nil).Maybe() h, req := tc.caller.handlerAndReq(store, tc.body) @@ -153,6 +154,7 @@ func TestAutoPurchaseGate_CreatePlan(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("CreatePurchasePlan", mock.Anything, mock.Anything).Return(nil).Maybe() store.On("SetPlanAccounts", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() store.GetCloudAccountFn = func(_ context.Context, id string) (*config.CloudAccount, error) { @@ -172,20 +174,29 @@ func TestAutoPurchaseGate_CreatePlan(t *testing.T) { } func TestAutoPurchaseGate_CapValidation(t *testing.T) { - for _, tc := range []struct{ name, body string }{ - {"zero", `{"name":"p","max_step_commitment":0}`}, - {"negative", `{"name":"p","max_step_commitment":-5}`}, - {"below one cent", `{"name":"p","max_step_commitment":0.004}`}, - {"1e308 overflows the column", `{"name":"p","max_step_commitment":1e308}`}, - {"quoted number", `{"name":"p","max_step_commitment":"100"}`}, - {"half cent 1.005", `{"name":"p","max_step_commitment":1.005}`}, - {"0.015", `{"name":"p","max_step_commitment":0.015}`}, - {"1.0049", `{"name":"p","max_step_commitment":1.0049}`}, - {"1e-3", `{"name":"p","max_step_commitment":1e-3}`}, + for _, tc := range []struct { + name, body string + // decodeRefused: the body does not decode, so the stored plan is + // never read. + decodeRefused bool + }{ + {"zero", `{"name":"p","max_step_commitment":0}`, false}, + {"negative", `{"name":"p","max_step_commitment":-5}`, false}, + {"below one cent", `{"name":"p","max_step_commitment":0.004}`, false}, + {"1e308 overflows the column", `{"name":"p","max_step_commitment":1e308}`, false}, + {"quoted number", `{"name":"p","max_step_commitment":"100"}`, true}, + {"half cent 1.005", `{"name":"p","max_step_commitment":1.005}`, false}, + {"0.015", `{"name":"p","max_step_commitment":0.015}`, false}, + {"1.0049", `{"name":"p","max_step_commitment":1.0049}`, false}, + {"1e-3", `{"name":"p","max_step_commitment":1e-3}`, false}, } { t.Run(tc.name, func(t *testing.T) { store := new(MockConfigStore) - store.On("GetPurchasePlan", mock.Anything, apgPlanID).Return(apgStoredPlan(false, nil), nil) + t.Cleanup(func() { store.AssertExpectations(t) }) + planRead := store.On("GetPurchasePlan", mock.Anything, apgPlanID).Return(apgStoredPlan(false, nil), nil) + if tc.decodeRefused { + planRead.Maybe() + } store.On("UpdatePurchasePlan", mock.Anything, mock.Anything).Return(nil).Maybe() h, req := apgCaller{perms: apgAdminPlus}.handlerAndReq(store, tc.body) _, err := h.updatePlan(context.Background(), req, apgPlanID) diff --git a/internal/api/claimed_row_check_test.go b/internal/api/claimed_row_check_test.go index fe06bc22f..75b631cab 100644 --- a/internal/api/claimed_row_check_test.go +++ b/internal/api/claimed_row_check_test.go @@ -70,9 +70,11 @@ func runNowClaimHarness(t *testing.T, mp *MockPurchaseManager, claimedUpfront fl execID := "11111111-1111-1111-1111-111111111111" snapshot := &config.PurchaseExecution{ExecutionID: execID, Status: "pending", Recommendations: claimRecs(claimUnder)} mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetExecutionByID", ctx, execID).Return(snapshot, nil) mockStore.On("GetGlobalConfig", mock.Anything).Return(&config.GlobalConfig{}, nil).Maybe() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "capped-token").Return(&Session{UserID: "dddddddd-dddd-dddd-dddd-dddddddddddd", Email: "capped@example.com"}, nil) mockAuth.grantPermissions([]auth.Permission{ {Action: auth.ActionAdmin, Resource: auth.ResourceAll}, @@ -89,6 +91,7 @@ func runNowClaimHarness(t *testing.T, mp *MockPurchaseManager, claimedUpfront fl func TestClaimedRowCheck_RunNow_RepricedRowRefused(t *testing.T) { mp := &MockPurchaseManager{} + t.Cleanup(func() { mp.AssertExpectations(t) }) err := runNowClaimHarness(t, mp, claimRepriced) requireClaimedRowRefused(t, mp, err) mp.AssertNotCalled(t, "RunPlannedPurchaseNow", mock.Anything, mock.Anything, mock.Anything, mock.Anything) @@ -96,6 +99,7 @@ func TestClaimedRowCheck_RunNow_RepricedRowRefused(t *testing.T) { func TestClaimedRowCheck_RunNow_UnmodifiedRowRuns(t *testing.T) { mp := &MockPurchaseManager{} + t.Cleanup(func() { mp.AssertExpectations(t) }) err := runNowClaimHarness(t, mp, claimUnder) require.NoError(t, err) requireOneCheck(t, mp) @@ -111,9 +115,11 @@ func sessionApproveClaimHarness(t *testing.T, mp *MockPurchaseManager, claimedUp approver := "approver@example.com" snapshot := &config.PurchaseExecution{ExecutionID: execID, Status: "pending", Recommendations: claimRecs(claimUnder)} mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) mockConfig.On("GetExecutionByID", ctx, execID).Return(snapshot, nil) mockConfig.On("GetGlobalConfig", mock.Anything).Return(&config.GlobalConfig{}, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{UserID: "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", Email: approver}, nil) mockAuth.On("ValidateCSRFToken", ctx, "sess-tok", "").Return(nil) mockAuth.grantPermissions([]auth.Permission{cappedPurchasePerm(auth.ActionApproveAny)}) @@ -127,6 +133,7 @@ func sessionApproveClaimHarness(t *testing.T, mp *MockPurchaseManager, claimedUp func TestClaimedRowCheck_SessionApprove_RepricedRowRefused(t *testing.T) { mp := &MockPurchaseManager{} + t.Cleanup(func() { mp.AssertExpectations(t) }) err := sessionApproveClaimHarness(t, mp, claimRepriced) requireClaimedRowRefused(t, mp, err) mp.AssertNotCalled(t, "ApproveAndExecute", mock.Anything, mock.Anything, mock.Anything, mock.Anything) @@ -134,6 +141,7 @@ func TestClaimedRowCheck_SessionApprove_RepricedRowRefused(t *testing.T) { func TestClaimedRowCheck_SessionApprove_UnmodifiedRowRuns(t *testing.T) { mp := &MockPurchaseManager{} + t.Cleanup(func() { mp.AssertExpectations(t) }) err := sessionApproveClaimHarness(t, mp, claimUnder) require.NoError(t, err) requireOneCheck(t, mp) @@ -196,7 +204,9 @@ func directExecuteClaimHarness(t *testing.T, mp *MockPurchaseManager, claimedUpf t.Helper() ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "direct@example.com"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(session, nil) mockAuth.grantPermissions([]auth.Permission{ @@ -221,6 +231,7 @@ func directExecuteClaimHarness(t *testing.T, mp *MockPurchaseManager, claimedUpf func TestClaimedRowCheck_DirectExecute_RepricedRowRefused(t *testing.T) { mp := &MockPurchaseManager{} + t.Cleanup(func() { mp.AssertExpectations(t) }) err := directExecuteClaimHarness(t, mp, claimRepriced) requireClaimedRowRefused(t, mp, err) mp.AssertNotCalled(t, "DirectExecute", mock.Anything, mock.Anything, mock.Anything, mock.Anything) @@ -228,6 +239,7 @@ func TestClaimedRowCheck_DirectExecute_RepricedRowRefused(t *testing.T) { func TestClaimedRowCheck_DirectExecute_UnmodifiedRowRuns(t *testing.T) { mp := &MockPurchaseManager{} + t.Cleanup(func() { mp.AssertExpectations(t) }) err := directExecuteClaimHarness(t, mp, 500) require.NoError(t, err) requireOneCheck(t, mp) diff --git a/internal/api/combined_plan_purchase_integration_test.go b/internal/api/combined_plan_purchase_integration_test.go index fe6937281..68955b9ba 100644 --- a/internal/api/combined_plan_purchase_integration_test.go +++ b/internal/api/combined_plan_purchase_integration_test.go @@ -135,6 +135,7 @@ func newCombinedPlanFixtureIn(t *testing.T, automatic bool, capAmount *float64, require.NoError(t, err) require.NoError(t, f.pool.QueryRow(ctx, `SELECT id FROM users WHERE email='creator@example.com'`).Scan(&f.creator)) creatorAuth := new(MockAuthService) + t.Cleanup(func() { creatorAuth.AssertExpectations(t) }) creatorAuth.On("ValidateSession", mock.Anything, "admin-token").Return(&Session{UserID: f.creator, Email: "creator@example.com"}, nil).Maybe() creatorAuth.On("ValidateCSRFToken", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() creatorAuth.grantAdminPurchaser() @@ -227,6 +228,7 @@ func (f *combinedPlanFixture) assertAttached(t *testing.T) { func (f *combinedPlanFixture) approveAs(t *testing.T, userID string, maxAmount float64) events.LambdaFunctionURLResponse { t.Helper() a := new(MockAuthService) + t.Cleanup(func() { a.AssertExpectations(t) }) a.On("ValidateSession", mock.Anything, "admin-token").Return(&Session{UserID: userID}, nil).Maybe() a.On("ValidateCSRFToken", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() a.grantPermissionsScoped([]auth.Permission{{Action: "approve-any", Resource: "purchases", Constraints: &auth.PermissionConstraints{AccountIDs: []string{f.account}, MaxPurchaseAmount: maxAmount}}}, []string{f.account}) diff --git a/internal/api/coverage_extras_test.go b/internal/api/coverage_extras_test.go index 861bf98a6..779ddd7dc 100644 --- a/internal/api/coverage_extras_test.go +++ b/internal/api/coverage_extras_test.go @@ -36,17 +36,15 @@ func TestHandler_approvePurchase_EmptyToken(t *testing.T) { // dispatch branch could pass the test without ever asserting on // `err`. // - // Wire a minimal MockConfigStore that returns a clean "execution - // not found" error from GetExecutionByID so the dispatch reaches a - // proper NewClientError(404) instead of nil-deref'ing on h.config. - // (Pre-#286 the empty-token check short-circuited at the very top - // of approvePurchase, so a zero-Handler test was sufficient. The - // dispatch refactor moved that check after GetExecutionByID; the - // test surface adapts in step.) + // approvePurchase now requires a session before it looks the + // execution up (requireSessionBeforeLookup), so the caller is refused + // with a 401 and the store is never read. The store has no + // expectations: a GetExecutionByID call would panic, which the recover + // below turns into a failure. ctx := context.Background() execID := "11111111-1111-1111-1111-111111111111" mockConfig := new(MockConfigStore) - mockConfig.On("GetExecutionByID", ctx, execID).Return(nil, errors.New("execution not found")) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) h := &Handler{config: mockConfig} defer func() { @@ -56,6 +54,9 @@ func TestHandler_approvePurchase_EmptyToken(t *testing.T) { }() _, err := h.approvePurchase(ctx, nil, execID, "") require.Error(t, err, "approvePurchase with empty token + zero handler must fail") + ce, ok := IsClientError(err) + require.True(t, ok, "expected a client error, got %v", err) + assert.Equal(t, 401, ce.code) } func TestHandler_approvePurchase_PurchaseError(t *testing.T) { @@ -64,6 +65,7 @@ func TestHandler_approvePurchase_PurchaseError(t *testing.T) { approver := "approver@example.com" mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) accountID := "acct-1" exec := &config.PurchaseExecution{ ExecutionID: execID, @@ -84,6 +86,7 @@ func TestHandler_approvePurchase_PurchaseError(t *testing.T) { mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{}, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, mock.Anything).Return(nil, nil).Maybe() mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: approver}, nil) @@ -100,6 +103,7 @@ func TestHandler_approvePurchase_PurchaseError(t *testing.T) { mockAuth.On("HasPermissionAPI", ctx, "", "approve-own", "purchases").Return(false, nil) mockPurchase := new(MockPurchaseManager) + t.Cleanup(func() { mockPurchase.AssertExpectations(t) }) mockPurchase.On("ApproveExecution", ctx, execID, "tok", approver). Return(nil, "", errors.New("approval failed")) @@ -127,6 +131,7 @@ func TestHandler_cancelPurchase_InvalidUUID(t *testing.T) { func TestHandler_cancelPurchase_EmptyToken_FallsThroughToSession(t *testing.T) { execID := "11111111-1111-1111-1111-111111111111" mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) h := &Handler{config: mockConfig} _, err := h.cancelPurchase(context.Background(), nil, execID, "") ce, ok := IsClientError(err) @@ -141,6 +146,7 @@ func TestHandler_cancelPurchase_PurchaseError(t *testing.T) { approver := "approver@example.com" mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) accountID := "acct-1" exec := &config.PurchaseExecution{ ExecutionID: execID, @@ -159,6 +165,7 @@ func TestHandler_cancelPurchase_PurchaseError(t *testing.T) { mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{}, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, mock.Anything).Return(nil, nil).Maybe() mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: approver}, nil) @@ -172,6 +179,7 @@ func TestHandler_cancelPurchase_PurchaseError(t *testing.T) { mockAuth.On("HasPermissionAPI", ctx, "", "cancel-own", "purchases").Return(false, nil).Maybe() mockPurchase := new(MockPurchaseManager) + t.Cleanup(func() { mockPurchase.AssertExpectations(t) }) mockPurchase.On("CancelExecution", ctx, execID, "tok", approver). Return(errors.New("cancel failed")) @@ -191,6 +199,7 @@ func TestHandler_cancelPurchase_PurchaseError(t *testing.T) { func TestHandler_forgotPassword_RateLimitExceeded(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) rl := NewInMemoryRateLimiter() rl.SetLimit("forgot_password", NewRateLimitConfig(1, 60)) @@ -252,6 +261,7 @@ func TestApplyOverrideSlices_NilFields(t *testing.T) { func TestHandler_sendPurchaseApprovalEmail_ConfigError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetGlobalConfig", ctx).Return(nil, errors.New("db down")) h := &Handler{ @@ -271,7 +281,9 @@ func TestHandler_sendPurchaseApprovalEmail_ConfigError(t *testing.T) { func TestHandler_updateRIExchangeConfig_GetGlobalConfigError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "uid"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) mockAuth.grantAdmin() @@ -290,7 +302,9 @@ func TestHandler_updateRIExchangeConfig_GetGlobalConfigError(t *testing.T) { func TestHandler_updateRIExchangeConfig_SaveError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "uid"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) mockAuth.grantAdmin() @@ -314,6 +328,7 @@ func TestHandler_updateRIExchangeConfig_SaveError(t *testing.T) { func TestHandler_rejectRIExchange_RecordNotFound(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetRIExchangeRecord", ctx, "11111111-1111-1111-1111-111111111111").Return(nil, nil) h := &Handler{config: mockStore} @@ -329,6 +344,7 @@ func TestHandler_rejectRIExchange_RecordNotFound(t *testing.T) { func TestHandler_rejectRIExchange_WrongToken(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetRIExchangeRecord", ctx, "11111111-1111-1111-1111-111111111111").Return( &config.RIExchangeRecord{ID: "11111111-1111-1111-1111-111111111111", ApprovalToken: config.HashApprovalToken("correct")}, nil) @@ -341,6 +357,7 @@ func TestHandler_rejectRIExchange_WrongToken(t *testing.T) { func TestHandler_rejectRIExchange_AlreadyProcessed(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetRIExchangeRecord", ctx, "11111111-1111-1111-1111-111111111111").Return( &config.RIExchangeRecord{ID: "11111111-1111-1111-1111-111111111111", ApprovalToken: config.HashApprovalToken("tok")}, nil) // Transition returns nil indicating already processed. @@ -361,6 +378,7 @@ func TestHandler_rejectRIExchange_AlreadyProcessed(t *testing.T) { func TestHandler_validateExchangeApproval_DBError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetRIExchangeRecord", ctx, "11111111-1111-1111-1111-111111111111"). Return(nil, errors.New("db error")) @@ -377,6 +395,7 @@ func TestHandler_validateExchangeApproval_DBError(t *testing.T) { func TestHandler_approveRIExchange_AlreadyProcessed(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetRIExchangeRecord", ctx, "11111111-1111-1111-1111-111111111111").Return( &config.RIExchangeRecord{ID: "11111111-1111-1111-1111-111111111111", ApprovalToken: config.HashApprovalToken("tok")}, nil) mockStore.On("TransitionRIExchangeStatus", ctx, "11111111-1111-1111-1111-111111111111", "pending", "processing", mock.Anything). diff --git a/internal/api/coverage_gaps_test.go b/internal/api/coverage_gaps_test.go index c4a8cd3aa..29dbcf40e 100644 --- a/internal/api/coverage_gaps_test.go +++ b/internal/api/coverage_gaps_test.go @@ -244,6 +244,7 @@ func TestHandler_checkRateLimit_Exceeded(t *testing.T) { func TestRequireAuth_UserAPIKey_InvalidKey(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateUserAPIKeyAPI", ctx, "bad-key"). Return(nil, nil, errors.New("invalid key")) @@ -280,6 +281,7 @@ func TestHandler_requireAdmin_NoAuthService(t *testing.T) { func TestHandler_requireAdmin_NoToken(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) h := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ Headers: map[string]string{}, @@ -292,6 +294,7 @@ func TestHandler_requireAdmin_NoToken(t *testing.T) { func TestHandler_requireAdmin_InvalidSession(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "bad-token").Return(nil, errors.New("expired")) h := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ @@ -362,6 +365,7 @@ func TestHandler_validateCSRF_AdminAPIKey(t *testing.T) { func TestHandler_validateCSRF_ValidUserAPIKey(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateUserAPIKeyAPI", ctx, "user-api-key"). Return("uid", map[string]interface{}{}, nil) h := &Handler{auth: mockAuth} @@ -375,6 +379,7 @@ func TestHandler_validateCSRF_ValidUserAPIKey(t *testing.T) { func TestHandler_validateCSRF_InvalidAPIKeyFallsThrough(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) // Invalid API key — also no bearer token → error about missing session mockAuth.On("ValidateUserAPIKeyAPI", ctx, "invalid-key"). Return(nil, nil, errors.New("bad key")) @@ -389,6 +394,7 @@ func TestHandler_validateCSRF_InvalidAPIKeyFallsThrough(t *testing.T) { func TestHandler_validateCSRF_NoSessionToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) h := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ Headers: map[string]string{}, @@ -401,6 +407,7 @@ func TestHandler_validateCSRF_NoSessionToken(t *testing.T) { func TestHandler_validateCSRF_ValidCSRF(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateCSRFToken", ctx, "session-tok", "csrf-tok").Return(nil) h := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ @@ -564,6 +571,7 @@ func TestHandler_checkCredentialPresence_WithCredStore_NotFound(t *testing.T) { func TestHandler_checkCredentialPresence_FallbackToConfigStore(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) acct := &config.CloudAccount{ID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Provider: "aws"} // MockConfigStore.HasAccountCredentials returns false by default @@ -624,6 +632,7 @@ func TestHandler_getRecommendations_InvalidAccountIDs(t *testing.T) { func TestHandler_getRecommendations_SchedulerError(t *testing.T) { ctx := context.Background() mockScheduler := new(MockScheduler) + t.Cleanup(func() { mockScheduler.AssertExpectations(t) }) mockScheduler.On("ListRecommendations", mock.Anything, mock.Anything). Return(nil, errors.New("scheduler down")) @@ -637,6 +646,7 @@ func TestHandler_getRecommendations_SchedulerError(t *testing.T) { func TestHandler_getRecommendations_WithResults(t *testing.T) { ctx := context.Background() mockScheduler := new(MockScheduler) + t.Cleanup(func() { mockScheduler.AssertExpectations(t) }) recs := []config.RecommendationRecord{ {Service: "rds", Region: "us-east-1", Savings: 100.0, UpfrontCost: 200.0}, {Service: "rds", Region: "eu-west-1", Savings: 50.0, UpfrontCost: 100.0}, @@ -659,6 +669,7 @@ func TestHandler_getRecommendations_WithResults(t *testing.T) { func TestHandler_getRecommendations_ZeroSavings(t *testing.T) { ctx := context.Background() mockScheduler := new(MockScheduler) + t.Cleanup(func() { mockScheduler.AssertExpectations(t) }) recs := []config.RecommendationRecord{ {Service: "rds", Region: "us-east-1", Savings: 0, UpfrontCost: 0}, } @@ -694,6 +705,7 @@ func TestHandler_sendPurchaseApprovalEmail_NilNotifier(t *testing.T) { func TestHandler_sendPurchaseApprovalEmail_NoNotificationEmail(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) // GetGlobalConfig returns a config without a notification email globalCfg := &config.GlobalConfig{} @@ -747,6 +759,7 @@ func TestHandler_sendPurchaseApprovalEmail_ResponseRecipientUsesNotificationEmai accountID := "acct-1" mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{ NotificationEmail: ¬ificationEmail, }, nil) @@ -788,6 +801,7 @@ func TestHandler_sendPurchaseApprovalEmail_ResponseRecipientFallsBackToContactEm accountID := "acct-2" mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{ // NotificationEmail intentionally not set. }, nil) diff --git a/internal/api/delete_planned_purchase_scope_test.go b/internal/api/delete_planned_purchase_scope_test.go index d7dfa1bdf..200d63253 100644 --- a/internal/api/delete_planned_purchase_scope_test.go +++ b/internal/api/delete_planned_purchase_scope_test.go @@ -246,7 +246,11 @@ func TestRouterDispatch_DeletePlannedPurchase_OutOfScopeExecutionRefusedBeforeAn func TestRouterDispatch_DeletePlannedPurchase_PermissionRefusedBeforeScope(t *testing.T) { f := newPlannedDeleteFixture(t, []config.CloudAccount{inScopeAccount(), outOfScopeAccount()}, scopedInAccount) store := f.store + // The refusal comes before the lookup the fixture stubs; the + // AssertNotCalled below pins that. + relaxUnreachedStubs(&store.Mock, "GetExecutionByID") mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, scopedToken).Return(&Session{UserID: scopedUserID}, nil).Maybe() mockAuth.grantPermissionsScoped([]auth.Permission{{Action: auth.ActionView, Resource: auth.ResourcePurchases}}, []string{scopedInAccount}) f.h.auth = mockAuth diff --git a/internal/api/executed_notification_flow_test.go b/internal/api/executed_notification_flow_test.go index 1228f3cd3..be728fef1 100644 --- a/internal/api/executed_notification_flow_test.go +++ b/internal/api/executed_notification_flow_test.go @@ -105,6 +105,7 @@ func TestExecutedNotification_TokenApprovePath(t *testing.T) { }, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, mock.Anything).Return(nil, nil).Maybe() mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: contact}, nil) @@ -178,6 +179,7 @@ func TestExecutedNotification_TokenApprovePath_RefetchFailureStillEmailsToken(t }, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, mock.Anything).Return(nil, nil).Maybe() mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: contact}, nil) @@ -226,6 +228,7 @@ func TestExecutedNotification_SessionApprovePath(t *testing.T) { freshToken := "session-revoke-token" mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) exec := approvalTestExec(execID, contact, mockConfig) mockConfig.On("GetExecutionByID", ctx, execID).Return(exec, nil) mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{ @@ -233,6 +236,7 @@ func TestExecutedNotification_SessionApprovePath(t *testing.T) { }, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: adminEmail}, nil) mockAuth.grantAdminPurchaser() mockAuth.On("ValidateCSRFToken", ctx, "sess-tok", "").Return(nil) @@ -281,6 +285,7 @@ func TestExecutedNotification_DirectExecutePath(t *testing.T) { freshToken := "direct-execute-revoke-token" mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) exec := &config.PurchaseExecution{ ExecutionID: execID, ApprovalToken: "valid-token", @@ -292,9 +297,9 @@ func TestExecutedNotification_DirectExecutePath(t *testing.T) { mockConfig.GetCloudAccountFn = func(_ context.Context, id string) (*config.CloudAccount, error) { return &config.CloudAccount{ID: id, ContactEmail: contact}, nil } - // directExecutePurchase stamps audit fields, then sendPurchaseExecutedEmail + // DirectExecute stamps the audit fields after its claim (the handler + // no longer saves the row itself), then sendPurchaseExecutedEmail // reads the global config. - mockConfig.On("SavePurchaseExecution", ctx, exec).Return(nil) mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{ NotificationEmail: &contact, }, nil) @@ -338,6 +343,7 @@ func TestExecutedNotification_DirectExecute_NilNotifierNoPanic(t *testing.T) { adminEmail := "admin@example.com" mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) exec := &config.PurchaseExecution{ ExecutionID: execID, ApprovalToken: "valid-token", @@ -346,7 +352,6 @@ func TestExecutedNotification_DirectExecute_NilNotifierNoPanic(t *testing.T) { {ID: "r1"}, }, } - mockConfig.On("SavePurchaseExecution", ctx, exec).Return(nil) mockPurchase := new(MockPurchaseManager) mockPurchase.On("DirectExecute", ctx, execID, adminEmail, (*string)(nil)).Return(nil, "", nil) diff --git a/internal/api/executed_notification_partial_test.go b/internal/api/executed_notification_partial_test.go index cf20a1816..15b530365 100644 --- a/internal/api/executed_notification_partial_test.go +++ b/internal/api/executed_notification_partial_test.go @@ -68,16 +68,19 @@ func assertPartialRun502(t *testing.T, err error) { assert.Equal(t, config.StatusPartiallyCompleted, ce.Details()["status"]) } -func sessionApproveHandler(ctx context.Context, execID, contact string, final *config.PurchaseExecution, token string, runErr error) (*Handler, *recordingExecutedNotifier) { +func sessionApproveHandler(t *testing.T, ctx context.Context, execID, contact string, final *config.PurchaseExecution, token string, runErr error) (*Handler, *recordingExecutedNotifier) { mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) exec := approvalTestExec(execID, contact, mockConfig) mockConfig.On("GetExecutionByID", ctx, execID).Return(exec, nil) mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{NotificationEmail: &contact}, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: "admin@example.com"}, nil) mockAuth.grantAdminPurchaser() mockAuth.On("ValidateCSRFToken", ctx, "sess-tok", "").Return(nil) mockPurchase := new(MockPurchaseManager) + t.Cleanup(func() { mockPurchase.AssertExpectations(t) }) mockPurchase.On("ApproveAndExecute", ctx, execID, "admin@example.com", (*string)(nil)).Return(final, token, runErr) notifier := &recordingExecutedNotifier{} return &Handler{purchase: mockPurchase, config: mockConfig, auth: mockAuth, emailNotifier: notifier}, notifier @@ -87,7 +90,7 @@ func TestExecutedNotification_PartialSessionApproveEmailsRevocationToken(t *test ctx := context.Background() execID := "56789012-5678-5678-5678-56789012abcd" row := partialRunRow(execID) - handler, notifier := sessionApproveHandler(ctx, execID, "contact@example.com", row, partialRunToken, errPartialRun) + handler, notifier := sessionApproveHandler(t, ctx, execID, "contact@example.com", row, partialRunToken, errPartialRun) _, err := handler.approvePurchase(ctx, &events.LambdaFunctionURLRequest{Headers: map[string]string{"authorization": "Bearer sess-tok"}}, execID, "") assertPartialRun502(t, err) @@ -110,7 +113,7 @@ func TestExecutedNotification_UncommittedFailureSendsNoEmail(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - handler, notifier := sessionApproveHandler(ctx, execID, "contact@example.com", tc.final, "", tc.err) + handler, notifier := sessionApproveHandler(t, ctx, execID, "contact@example.com", tc.final, "", tc.err) _, err := handler.approvePurchase(ctx, &events.LambdaFunctionURLRequest{Headers: map[string]string{"authorization": "Bearer sess-tok"}}, execID, "") require.Error(t, err) assert.Zero(t, notifier.calls) @@ -125,11 +128,13 @@ func TestExecutedNotification_PartialDirectExecuteEmailsRevocationToken(t *testi row := partialRunRow(execID) mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) mockConfig.GetCloudAccountFn = func(_ context.Context, id string) (*config.CloudAccount, error) { return &config.CloudAccount{ID: id, ContactEmail: contact}, nil } mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{NotificationEmail: &contact}, nil) mockPurchase := new(MockPurchaseManager) + t.Cleanup(func() { mockPurchase.AssertExpectations(t) }) mockPurchase.On("DirectExecute", ctx, execID, "admin@example.com", (*string)(nil)).Return(row, partialRunToken, errPartialRun) notifier := &recordingExecutedNotifier{} handler := &Handler{purchase: mockPurchase, config: mockConfig, emailNotifier: notifier} @@ -145,18 +150,21 @@ func TestExecutedNotification_PartialDirectExecuteEmailsRevocationToken(t *testi // takes the token branch, whose ApproveExecution returns final, token and // runErr. The execution is read once, for the request itself: a failed run // answers from the returned row, never from a re-fetch. -func tokenApproveHandler(ctx context.Context, execID, contact string, final *config.PurchaseExecution, token string, runErr error) (*Handler, *recordingExecutedNotifier, *MockConfigStore) { +func tokenApproveHandler(t *testing.T, ctx context.Context, execID, contact string, final *config.PurchaseExecution, token string, runErr error) (*Handler, *recordingExecutedNotifier, *MockConfigStore) { mockConfig := new(MockConfigStore) + t.Cleanup(func() { mockConfig.AssertExpectations(t) }) exec := approvalTestExec(execID, contact, mockConfig) mockConfig.On("GetExecutionByID", ctx, execID).Return(exec, nil).Once() mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{NotificationEmail: &contact}, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, mock.Anything).Return(nil, nil).Maybe() mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: contact}, nil) mockAuth.On("ValidateCSRFToken", ctx, "sess-tok", "").Return(nil) mockAuth.On("HasPermissionAPI", ctx, "", "approve-any", "purchases").Return(false, nil).Maybe() mockAuth.On("HasPermissionAPI", ctx, "", "approve-own", "purchases").Return(false, nil).Maybe() mockPurchase := new(MockPurchaseManager) + t.Cleanup(func() { mockPurchase.AssertExpectations(t) }) mockPurchase.On("ApproveExecution", ctx, execID, "valid-token", contact).Return(final, token, runErr) notifier := &recordingExecutedNotifier{} return &Handler{purchase: mockPurchase, config: mockConfig, auth: mockAuth, emailNotifier: notifier}, notifier, mockConfig @@ -168,7 +176,7 @@ func TestExecutedNotification_PartialTokenApproveEmailsRevocationToken(t *testin ctx := context.Background() execID := "89012345-8901-8901-8901-89012345abcd" row := partialRunRow(execID) - handler, notifier, mockConfig := tokenApproveHandler(ctx, execID, "contact@example.com", row, partialRunToken, errPartialRun) + handler, notifier, mockConfig := tokenApproveHandler(t, ctx, execID, "contact@example.com", row, partialRunToken, errPartialRun) _, err := handler.approvePurchase(ctx, &events.LambdaFunctionURLRequest{Headers: map[string]string{"authorization": "Bearer sess-tok"}}, execID, "valid-token") assertPartialRun502(t, err) @@ -201,7 +209,7 @@ func TestTokenApprove_FailureMappedByCause(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - handler, notifier, _ := tokenApproveHandler(ctx, execID, "contact@example.com", tc.final, "", tc.err) + handler, notifier, _ := tokenApproveHandler(t, ctx, execID, "contact@example.com", tc.final, "", tc.err) _, err := handler.approvePurchase(ctx, &events.LambdaFunctionURLRequest{Headers: map[string]string{"authorization": "Bearer sess-tok"}}, execID, "valid-token") require.Error(t, err) assert.Zero(t, notifier.calls) @@ -230,12 +238,14 @@ func TestExecutedNotification_TokenApproveFailureWithoutTokenSendsNoEmail(t *tes mockConfig.On("GetExecutionByID", ctx, execID).Return(exec, nil).Once() mockConfig.On("GetGlobalConfig", ctx).Return(&config.GlobalConfig{NotificationEmail: &contact}, nil) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, mock.Anything).Return(nil, nil).Maybe() mockAuth.On("ValidateSession", ctx, "sess-tok").Return(&Session{Email: contact}, nil) mockAuth.On("ValidateCSRFToken", ctx, "sess-tok", "").Return(nil) mockAuth.On("HasPermissionAPI", ctx, "", "approve-any", "purchases").Return(false, nil).Maybe() mockAuth.On("HasPermissionAPI", ctx, "", "approve-own", "purchases").Return(false, nil).Maybe() mockPurchase := new(MockPurchaseManager) + t.Cleanup(func() { mockPurchase.AssertExpectations(t) }) mockPurchase.On("ApproveExecution", ctx, execID, "valid-token", contact).Return(nil, "", errors.New("AWS RI purchase failed")) notifier := &recordingExecutedNotifier{} handler := &Handler{purchase: mockPurchase, config: mockConfig, auth: mockAuth, emailNotifier: notifier} diff --git a/internal/api/execution_scope_test.go b/internal/api/execution_scope_test.go index 7186a2c15..6ca63b885 100644 --- a/internal/api/execution_scope_test.go +++ b/internal/api/execution_scope_test.go @@ -174,7 +174,14 @@ func scopeTestExecution(k scopeTestKind, status string) *config.PurchaseExecutio // sites for TestNoUnfailableMockAssertions. func newScopeTestHandler(t *testing.T, exec *config.PurchaseExecution, scope []string, store *MockConfigStore, mockPurchase *MockPurchaseManager) *Handler { t.Helper() + // Every caller's store and purchase manager are asserted here, so a + // caller passing a bare new(...) still has its expectations verified. + t.Cleanup(func() { + store.AssertExpectations(t) + mockPurchase.AssertExpectations(t) + }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, "operator-token").Return(&Session{ UserID: "operator-1", Email: "operator@example.com", @@ -411,6 +418,9 @@ func TestExecutionAccounts_AdHocUnattributedDenied(t *testing.T) { scopeTestRec("rec-none", missing), }}, "pending") h := newScopeTestHandler(t, exec, []string{scopeTestProd.ID, scopeTestStage.ID}, new(MockConfigStore), new(MockPurchaseManager)) + // The gate is called directly with a session, so no bearer token is + // validated. + relaxUnreachedStubs(&h.auth.(*MockAuthService).Mock, "ValidateSession") err := h.requireExecutionAccess(context.Background(), &Session{UserID: "operator-1"}, scopeTestExecID) assert.True(t, IsNotFoundError(err), "expected 404 not-found, got %v", err) } @@ -430,6 +440,8 @@ func TestPlannedPurchases_HidesPartiallyScopedParent(t *testing.T) { exec := scopeTestExecution(scopeTestKinds[0], "pending") store := new(MockConfigStore) h := newScopeTestHandler(t, exec, tc.scope, store, new(MockPurchaseManager)) + // The list reads the planned rows, not one execution by ID. + relaxUnreachedStubs(&store.Mock, "GetExecutionByID") store.On("GetPlannedExecutions", mock.Anything, mock.Anything, mock.Anything).Return([]config.PurchaseExecution{*exec}, nil) store.On("ListPurchasePlans", mock.Anything, mock.Anything).Return([]config.PurchasePlan{{ID: scopeTestPlanID, Name: "p"}}, nil) diff --git a/internal/api/four_eyes_unattributed_creator_test.go b/internal/api/four_eyes_unattributed_creator_test.go index f1ac4ab94..2267850cf 100644 --- a/internal/api/four_eyes_unattributed_creator_test.go +++ b/internal/api/four_eyes_unattributed_creator_test.go @@ -50,10 +50,13 @@ func TestExecutePurchase_AdminAPIKey_FourEyesOn_RefusedBeforeAnyWrite(t *testing } { ctx := context.Background() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOn(), nil) expectStoredRecs(store, config.RecommendationRecord{ ID: "rec-1", Provider: "aws", Service: "ec2", Count: 1, Term: 1, Payment: "all-upfront", UpfrontCost: 500, Savings: 100, }) + // The unattributed caller is refused before the stored-price lookup. + relaxUnreachedStubs(&store.Mock, "ListStoredRecommendations") handler := &Handler{config: store, apiKey: fourEyesAdminKey} _, err := handler.executePurchase(ctx, adminKeyRequest(body)) @@ -68,6 +71,7 @@ func TestExecutePurchase_AdminAPIKey_FourEyesOn_RefusedBeforeAnyWrite(t *testing func TestExecutePurchase_AdminAPIKey_FourEyesOff_ReachesPersistence(t *testing.T) { ctx := context.Background() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOff(), nil) expectStoredRecs(store, config.RecommendationRecord{ ID: "rec-1", Provider: "aws", Service: "ec2", Count: 1, Term: 1, Payment: "all-upfront", UpfrontCost: 500, Savings: 100, @@ -91,6 +95,7 @@ func TestRetryPurchase_UnattributedSession_FourEyesOn_RefusedBeforeAnyWrite(t *t } ctx := context.Background() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetExecutionByID", ctx, retryExecID).Return(failed, nil) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOn(), nil) handler := &Handler{config: store, auth: unattributedRetryAuth(t)} @@ -114,6 +119,7 @@ func TestRetryPurchase_UnattributedSession_FourEyesOff_ReachesPersistence(t *tes } ctx := context.Background() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetExecutionByID", ctx, retryExecID).Return(failed, nil) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOff(), nil) store.On("WithTx", ctx, mock.Anything).Return(errPersistReached) @@ -132,6 +138,7 @@ func unattributedRetryAuth(t *testing.T) *MockAuthService { t.Helper() session := &Session{UserID: "service-principal"} auth := new(MockAuthService) + t.Cleanup(func() { auth.AssertExpectations(t) }) auth.On("ValidateSession", mock.Anything, "sess-tok").Return(session, nil) auth.On("HasPermissionAPI", mock.Anything, session.UserID, "retry-any", "purchases").Return(true, nil).Maybe() auth.On("GetAllowedAccountsAPI", mock.Anything, session.UserID).Return([]string{}, nil).Maybe() @@ -144,6 +151,7 @@ const fourEyesPlanID = "11111111-1111-1111-1111-111111111111" func TestCreatePlannedPurchases_AdminAPIKey_FourEyesOn_RefusedBeforeAnyWrite(t *testing.T) { ctx := context.Background() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOn(), nil) handler := &Handler{config: store, apiKey: fourEyesAdminKey} @@ -157,6 +165,7 @@ func TestCreatePlannedPurchases_AdminAPIKey_FourEyesOn_RefusedBeforeAnyWrite(t * func TestCreatePlannedPurchases_AdminAPIKey_FourEyesOff_ReachesPersistence(t *testing.T) { ctx := context.Background() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOff(), nil) store.On("GetPurchasePlan", ctx, fourEyesPlanID).Return(&config.PurchasePlan{ID: fourEyesPlanID, Enabled: true}, nil) store.On("WithTx", ctx, mock.Anything).Return(errPersistReached) @@ -174,6 +183,7 @@ func TestRequireAttributablePurchaseCreator(t *testing.T) { t.Run("user session passes without reading config", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) h := &Handler{config: store} require.NoError(t, h.requireAttributablePurchaseCreator(ctx, &Session{UserID: retryCallerID, Email: "u@example.com"})) require.NoError(t, h.requireAttributablePurchaseCreator(ctx, &Session{UserID: retryCallerID, UserAPIKeyID: "key-1"})) @@ -182,6 +192,7 @@ func TestRequireAttributablePurchaseCreator(t *testing.T) { t.Run("config load failure fails closed", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(nil, errors.New("db down")) h := &Handler{config: store} err := h.requireAttributablePurchaseCreator(ctx, adminKey) @@ -191,6 +202,7 @@ func TestRequireAttributablePurchaseCreator(t *testing.T) { t.Run("nil config means 4-eyes off", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(nil, nil) h := &Handler{config: store} require.NoError(t, h.requireAttributablePurchaseCreator(ctx, adminKey)) @@ -198,6 +210,7 @@ func TestRequireAttributablePurchaseCreator(t *testing.T) { t.Run("nil session under 4-eyes is refused", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOn(), nil) h := &Handler{config: store} requireFourEyesUnattributedRefusal(t, h.requireAttributablePurchaseCreator(ctx, nil)) @@ -205,6 +218,7 @@ func TestRequireAttributablePurchaseCreator(t *testing.T) { t.Run("non-UUID user id under 4-eyes is refused", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGlobalConfig", ctx).Return(fourEyesCfgOn(), nil) h := &Handler{config: store} requireFourEyesUnattributedRefusal(t, h.requireAttributablePurchaseCreator(ctx, &Session{UserID: "not-a-uuid"})) diff --git a/internal/api/global_config_arming_grants_test.go b/internal/api/global_config_arming_grants_test.go index 5e7f93bf4..9887ed286 100644 --- a/internal/api/global_config_arming_grants_test.go +++ b/internal/api/global_config_arming_grants_test.go @@ -113,6 +113,7 @@ func TestGlobalConfigArmingGates_LookupErrorOnlyMattersOnEscalation(t *testing.T lookupErr := errors.New("auth store unavailable") newHandler := func() (*Handler, *authFreeTxStore) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, "tok").Return(&Session{UserID: apgUserID}, nil).Maybe() mockAuth.On("HasPermissionAPI", mock.Anything, apgUserID, auth.ActionExecute, mock.Anything).Return(false, lookupErr) mockAuth.grantPermissions(afAdmin) diff --git a/internal/api/grantadmin_carveout_test.go b/internal/api/grantadmin_carveout_test.go index 19215ceac..5952c6b35 100644 --- a/internal/api/grantadmin_carveout_test.go +++ b/internal/api/grantadmin_carveout_test.go @@ -134,6 +134,7 @@ func TestGrantAdminPurchaser_GrantsCarvedOutVerbs(t *testing.T) { func TestExecutePurchase_PlainAdminIsRefused(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) t.Cleanup(func() { mockAuth.AssertExpectations(t) }) diff --git a/internal/api/grantscoped_test.go b/internal/api/grantscoped_test.go index 4ca763634..41ef4fd17 100644 --- a/internal/api/grantscoped_test.go +++ b/internal/api/grantscoped_test.go @@ -32,6 +32,7 @@ const ( func scopedHandler(t *testing.T, accounts ...string) (*Handler, *MockConfigStore) { t.Helper() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) t.Cleanup(func() { mockAuth.AssertExpectations(t) }) diff --git a/internal/api/group_explicit_zero_cap_test.go b/internal/api/group_explicit_zero_cap_test.go index 40b54beec..a375663ca 100644 --- a/internal/api/group_explicit_zero_cap_test.go +++ b/internal/api/group_explicit_zero_cap_test.go @@ -56,6 +56,7 @@ func TestGroupWritesHTTP_ExplicitZeroSpendCap(t *testing.T) { for _, tc := range cases { run := func(t *testing.T, write func(h *Handler, req *events.LambdaFunctionURLRequest) (any, error)) { store := new(MockStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetGroup", mock.Anything, zeroCapGroupID).Return(&auth.Group{ID: zeroCapGroupID, Name: "Team"}, nil).Maybe() store.On("CreateGroup", mock.Anything, mock.Anything).Return(nil).Maybe() store.On("UpdateGroup", mock.Anything, mock.Anything).Return(nil).Maybe() @@ -63,6 +64,7 @@ func TestGroupWritesHTTP_ExplicitZeroSpendCap(t *testing.T) { // issue #1409 member check lists users; the group has none. store.On("ListUsers", mock.Anything).Return([]auth.User{}, nil).Maybe() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, "admin-token").Return(&Session{UserID: auth.AdminAPIKeyActorID}, nil) mockAuth.grantAdmin() h := &Handler{auth: &realGroupAuth{MockAuthService: mockAuth, svc: auth.NewService(auth.ServiceConfig{Store: store})}} diff --git a/internal/api/handler_accounts_awsprobe_test.go b/internal/api/handler_accounts_awsprobe_test.go index 9380cdd29..12ec325ba 100644 --- a/internal/api/handler_accounts_awsprobe_test.go +++ b/internal/api/handler_accounts_awsprobe_test.go @@ -172,10 +172,12 @@ func runAccountTest(t *testing.T, pc probeCase) AccountTestResult { t.Helper() ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{UserID: "uid"}, nil) mockAuth.grantAdmin() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) acct := pc.acct acct.ID = probeAccountID acct.Provider = "aws" diff --git a/internal/api/handler_accounts_controlchar_test.go b/internal/api/handler_accounts_controlchar_test.go index 76873ad04..92e47b891 100644 --- a/internal/api/handler_accounts_controlchar_test.go +++ b/internal/api/handler_accounts_controlchar_test.go @@ -52,8 +52,10 @@ func TestCreateAccount_RejectsControlChars(t *testing.T) { for _, field := range []string{"name", "external_id"} { t.Run(field+"/"+label, func(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} name, ext := "Acme", "acme-prod" @@ -80,8 +82,10 @@ func TestUpdateAccount_RejectsControlChars(t *testing.T) { for _, field := range []string{"name", "external_id"} { t.Run(field+"/"+label, func(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} name, ext := "Acme", "acme-prod" @@ -108,6 +112,7 @@ func TestCreateAccount_AcceptsPrintableNames(t *testing.T) { ctx := context.Background() for _, name := range []string{"Acme Production", "Acme (EU) – Zürich #2", "日本 本番"} { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) handler := &Handler{auth: mockAuth, config: new(MockConfigStore)} result, err := handler.createAccount(ctx, adminRequest(accountBody(t, name, "acme-prod"))) @@ -125,6 +130,7 @@ func TestSubmitRegistration_RejectsControlChars(t *testing.T) { submit := func(t *testing.T, name, ext string) error { t.Helper() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) h := &Handler{config: store} b, err := json.Marshal(map[string]string{ "provider": "azure", "external_id": ext, "account_name": name, "contact_email": "ops@example.com", diff --git a/internal/api/handler_accounts_external_id_test.go b/internal/api/handler_accounts_external_id_test.go index 0410607d2..d0f83a380 100644 --- a/internal/api/handler_accounts_external_id_test.go +++ b/internal/api/handler_accounts_external_id_test.go @@ -68,8 +68,10 @@ func TestValidateAWSExternalID(t *testing.T) { func TestCreateAccount_AWSExternalID_RoleArnRequired(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012",` + @@ -89,8 +91,10 @@ func TestCreateAccount_AWSExternalID_RoleArnRequired(t *testing.T) { func TestCreateAccount_AWSExternalID_RoleArnTooShort(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012",` + @@ -110,8 +114,10 @@ func TestCreateAccount_AWSExternalID_RoleArnTooShort(t *testing.T) { func TestCreateAccount_AWSExternalID_RoleArnInvalidChars(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012",` + @@ -131,8 +137,10 @@ func TestCreateAccount_AWSExternalID_RoleArnInvalidChars(t *testing.T) { func TestCreateAccount_AWSExternalID_RoleArnHappyPath(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012",` + @@ -150,7 +158,7 @@ func TestCreateAccount_AWSExternalID_RoleArnHappyPath(t *testing.T) { // fake host account, which that shape now requires (#531). func TestCreateAccount_AWSExternalID_RoleArnNoRoleArnSelfAccount(t *testing.T) { ctx := context.Background() - handler := hostCheckHandler(setupAdminMock(ctx), &probeSTS{}, nil) + handler := hostCheckHandler(t, setupAdminMock(ctx), &probeSTS{}, nil) body := `{"name":"CUDly host","provider":"aws","external_id":"` + probeHostAcct + `",` + `"aws_auth_mode":"role_arn"}` // no aws_role_arn, no aws_external_id @@ -170,8 +178,10 @@ func TestCreateAccount_AWSExternalID_NotRequiredForOtherAuthModes(t *testing.T) t.Run(mode, func(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012",` + @@ -190,8 +200,10 @@ func TestCreateAccount_AWSExternalID_NotRequiredForOtherAuthModes(t *testing.T) func TestCreateAccount_AWSExternalID_BastionRequiresExternalID(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Target","provider":"aws","external_id":"123456789012",` + @@ -210,8 +222,10 @@ func TestCreateAccount_AWSExternalID_BastionRequiresExternalID(t *testing.T) { func TestCreateAccount_AWSExternalID_BastionHappyPath(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Target","provider":"aws","external_id":"123456789012",` + @@ -231,8 +245,10 @@ func TestCreateAccount_AWSExternalID_BastionHappyPath(t *testing.T) { func TestCreateAccount_AWSExternalID_BastionNoRoleArnExempt(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Target","provider":"aws","external_id":"123456789012",` + diff --git a/internal/api/handler_accounts_hostcheck_test.go b/internal/api/handler_accounts_hostcheck_test.go index a9bcfc847..22a0ab0e1 100644 --- a/internal/api/handler_accounts_hostcheck_test.go +++ b/internal/api/handler_accounts_hostcheck_test.go @@ -26,8 +26,10 @@ const hostCheckRegID = "33333333-3333-3333-3333-333333333333" // hostCheckHandler returns a handler whose cached host AWS config signs with // the probe host keys and sends every STS call to fake (no real AWS). -func hostCheckHandler(store *MockConfigStore, fake aws.HTTPClient, cfgErr error) *Handler { +func hostCheckHandler(t *testing.T, store *MockConfigStore, fake aws.HTTPClient, cfgErr error) *Handler { + t.Helper() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(context.Background(), mockAuth) h := &Handler{auth: mockAuth, config: store} h.awsCfgOnce.Do(func() { @@ -100,7 +102,8 @@ func TestCreateAccount_EmptyRoleARNRequiresHostAccount(t *testing.T) { for _, tc := range rejectedHostCheckCases() { t.Run(tc.name, func(t *testing.T) { store := new(MockConfigStore) - h := hostCheckHandler(store, tc.fake, tc.cfgErr) + t.Cleanup(func() { store.AssertExpectations(t) }) + h := hostCheckHandler(t, store, tc.fake, tc.cfgErr) result, err := h.createAccount(ctx, adminRequest(roleARNBody(tc.externalID, ""))) assert.Nil(t, result) requireNotHost400(t, err, tc.wantMsg) @@ -110,8 +113,9 @@ func TestCreateAccount_EmptyRoleARNRequiresHostAccount(t *testing.T) { t.Run("host account is accepted", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) fake := &probeSTS{} - h := hostCheckHandler(store, fake, nil) + h := hostCheckHandler(t, store, fake, nil) _, err := h.createAccount(ctx, adminRequest(roleARNBody(probeHostAcct, ""))) require.NoError(t, err) store.AssertCalled(t, "CreateCloudAccount") @@ -120,8 +124,9 @@ func TestCreateAccount_EmptyRoleARNRequiresHostAccount(t *testing.T) { t.Run("role ARN set skips the host check", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) fake := &probeSTS{transportErr: true} - h := hostCheckHandler(store, fake, nil) + h := hostCheckHandler(t, store, fake, nil) _, err := h.createAccount(ctx, adminRequest(roleARNBody(probeTenantAcct, probeTenantRole))) require.NoError(t, err) store.AssertCalled(t, "CreateCloudAccount") @@ -130,8 +135,9 @@ func TestCreateAccount_EmptyRoleARNRequiresHostAccount(t *testing.T) { t.Run("other auth modes skip the host check", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) fake := &probeSTS{transportErr: true} - h := hostCheckHandler(store, fake, nil) + h := hostCheckHandler(t, store, fake, nil) body := `{"name":"Acme","provider":"aws","external_id":"` + probeTenantAcct + `","aws_auth_mode":"access_keys"}` _, err := h.createAccount(ctx, adminRequest(body)) require.NoError(t, err) @@ -144,7 +150,8 @@ func TestUpdateAccount_EmptyRoleARNRequiresHostAccount(t *testing.T) { for _, tc := range rejectedHostCheckCases() { t.Run(tc.name, func(t *testing.T) { store := new(MockConfigStore) - h := hostCheckHandler(store, tc.fake, tc.cfgErr) + t.Cleanup(func() { store.AssertExpectations(t) }) + h := hostCheckHandler(t, store, tc.fake, tc.cfgErr) result, err := h.updateAccount(ctx, adminRequest(roleARNBody(tc.externalID, "")), probeAccountID) assert.Nil(t, result) requireNotHost400(t, err, tc.wantMsg) @@ -154,7 +161,8 @@ func TestUpdateAccount_EmptyRoleARNRequiresHostAccount(t *testing.T) { t.Run("host account is accepted", func(t *testing.T) { store := new(MockConfigStore) - h := hostCheckHandler(store, &probeSTS{}, nil) + t.Cleanup(func() { store.AssertExpectations(t) }) + h := hostCheckHandler(t, store, &probeSTS{}, nil) _, err := h.updateAccount(ctx, adminRequest(roleARNBody(probeHostAcct, "")), probeAccountID) require.NoError(t, err) store.AssertCalled(t, "UpdateCloudAccount") @@ -162,9 +170,10 @@ func TestUpdateAccount_EmptyRoleARNRequiresHostAccount(t *testing.T) { t.Run("out-of-scope account is not probed", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(context.Context, string) (*config.CloudAccount, error) { return nil, nil } fake := &probeSTS{} - h := hostCheckHandler(store, fake, nil) + h := hostCheckHandler(t, store, fake, nil) _, err := h.updateAccount(ctx, adminRequest(roleARNBody(probeHostAcct, "")), probeAccountID) require.ErrorIs(t, err, errNotFound) assert.Equal(t, int32(0), fake.identityCalls.Load()) @@ -176,13 +185,17 @@ func TestApproveRegistration_EmptyRoleARNRequiresHostAccount(t *testing.T) { for _, tc := range rejectedHostCheckCases() { t.Run(tc.name, func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetAccountRegistration", mock.Anything, hostCheckRegID). Return(&config.AccountRegistration{ID: hostCheckRegID, Status: "pending", Provider: "aws", ExternalID: tc.externalID}, nil) // Registered so a regression fails on the assertions below // instead of panicking on an unexpected call. store.On("ApproveAccountRegistration", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(errors.New("registration must not be approved")) - h := hostCheckHandler(store, tc.fake, tc.cfgErr) + Return(errors.New("registration must not be approved")).Maybe() + h := hostCheckHandler(t, store, tc.fake, tc.cfgErr) + // approveRegistration is called directly, without the router's + // session check. + relaxUnreachedStubs(&h.auth.(*MockAuthService).Mock, "ValidateSession") req := &events.LambdaFunctionURLRequest{Body: roleARNBody(tc.externalID, "")} result, err := h.approveRegistration(ctx, req, hostCheckRegID) assert.Nil(t, result) @@ -195,10 +208,12 @@ func TestApproveRegistration_EmptyRoleARNRequiresHostAccount(t *testing.T) { t.Run("host account is accepted", func(t *testing.T) { store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetAccountRegistration", mock.Anything, hostCheckRegID). Return(&config.AccountRegistration{ID: hostCheckRegID, Status: "pending", Provider: "aws", ExternalID: probeHostAcct}, nil) store.On("ApproveAccountRegistration", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - h := hostCheckHandler(store, &probeSTS{}, nil) + h := hostCheckHandler(t, store, &probeSTS{}, nil) + relaxUnreachedStubs(&h.auth.(*MockAuthService).Mock, "ValidateSession") req := &events.LambdaFunctionURLRequest{Body: roleARNBody(probeHostAcct, "")} _, err := h.approveRegistration(ctx, req, hostCheckRegID) require.NoError(t, err) diff --git a/internal/api/handler_accounts_partition_notice_test.go b/internal/api/handler_accounts_partition_notice_test.go index 3d2b9aea7..c8bddfc88 100644 --- a/internal/api/handler_accounts_partition_notice_test.go +++ b/internal/api/handler_accounts_partition_notice_test.go @@ -50,6 +50,7 @@ func roleARNAccountBody(roleARN string) string { func TestCreateAccount_AWSChinaRoleARN_ReturnsUnsupportedPartitionNotice(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) handler := &Handler{auth: mockAuth, config: setupAdminMock(ctx)} @@ -61,6 +62,7 @@ func TestCreateAccount_AWSChinaRoleARN_ReturnsUnsupportedPartitionNotice(t *test func TestCreateAccount_CommercialRoleARN_HasNoNotice(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) handler := &Handler{auth: mockAuth, config: setupAdminMock(ctx)} @@ -72,6 +74,7 @@ func TestCreateAccount_CommercialRoleARN_HasNoNotice(t *testing.T) { func TestUpdateAccount_AWSChinaRoleARN_ReturnsUnsupportedPartitionNotice(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) existing := sampleAccount() handler := &Handler{auth: mockAuth, config: &mockConfigStoreAccounts{MockConfigStore: setupAdminMock(ctx), getResult: &existing}} @@ -84,6 +87,7 @@ func TestUpdateAccount_AWSChinaRoleARN_ReturnsUnsupportedPartitionNotice(t *test func TestGetAccount_AWSChinaRoleARN_ReturnsUnsupportedPartitionNotice(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) acct := sampleAccount() acct.AWSAuthMode = "role_arn" @@ -111,6 +115,7 @@ func TestListAccounts_PartitionNotices(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) cn := sampleAccount() diff --git a/internal/api/handler_accounts_provider_immutable_test.go b/internal/api/handler_accounts_provider_immutable_test.go index 497f6ef97..b957e810e 100644 --- a/internal/api/handler_accounts_provider_immutable_test.go +++ b/internal/api/handler_accounts_provider_immutable_test.go @@ -27,10 +27,12 @@ func updateAccountRequest(body, id string) *events.LambdaFunctionURLRequest { func TestUpdateAccount_ProviderChangeIsRejected(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) existing := sampleAccount() // provider aws store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetCloudAccount", ctx, existing.ID).Return(&existing, nil) // Registered so the pre-fix handler reaches the save and returns the // echoed account instead of panicking on an unexpected call. @@ -53,6 +55,7 @@ func TestUpdateAccount_ProviderChangeIsRejected(t *testing.T) { func TestUpdateAccount_SameProviderIsSaved(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) existing := sampleAccount() diff --git a/internal/api/handler_accounts_router_test.go b/internal/api/handler_accounts_router_test.go index e297a5df0..52662d46e 100644 --- a/internal/api/handler_accounts_router_test.go +++ b/internal/api/handler_accounts_router_test.go @@ -20,11 +20,14 @@ func routerReq(method, path, body string) (*events.LambdaFunctionURLRequest, str // setupRouterForDispatch returns a Router whose Handler has admin auth configured. // The MockConfigStore uses hardcoded stubs for account methods; the tests verify // dispatch decisions, not end-to-end handler outcomes. -func setupRouterForDispatch(ctx context.Context) *Router { +func setupRouterForDispatch(t *testing.T, ctx context.Context) *Router { + t.Helper() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminAccountSession(), nil) mockAuth.grantAdmin() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) h := &Handler{auth: mockAuth, config: store} return NewRouter(h) } @@ -37,7 +40,7 @@ func setupRouterForDispatch(ctx context.Context) *Router { // the newly built override — the important thing is no client/not-found error. func TestRouterDispatch_UpdateServiceOverride(t *testing.T) { ctx := context.Background() - r := setupRouterForDispatch(ctx) + r := setupRouterForDispatch(t, ctx) accountID := "11111111-1111-1111-1111-111111111111" req, method, path := routerReq("PUT", "/api/accounts/"+accountID+"/service-overrides/aws/ec2", `{"term":1}`) @@ -51,7 +54,7 @@ func TestRouterDispatch_UpdateServiceOverride(t *testing.T) { // valid account so updateAccount succeeds — confirming the correct handler ran. func TestRouterDispatch_UpdateAccount_RoutesCorrectly(t *testing.T) { ctx := context.Background() - r := setupRouterForDispatch(ctx) + r := setupRouterForDispatch(t, ctx) accountID := "11111111-1111-1111-1111-111111111111" req, method, path := routerReq("PUT", "/api/accounts/"+accountID, `{"name":"Updated","provider":"aws","external_id":"123456789012"}`) @@ -65,7 +68,7 @@ func TestRouterDispatch_UpdateAccount_RoutesCorrectly(t *testing.T) { // routes to deleteAccountServiceOverride. func TestRouterDispatch_DeleteServiceOverride(t *testing.T) { ctx := context.Background() - r := setupRouterForDispatch(ctx) + r := setupRouterForDispatch(t, ctx) accountID := "11111111-1111-1111-1111-111111111111" req, method, path := routerReq("DELETE", "/api/accounts/"+accountID+"/service-overrides/aws/ec2", "") @@ -80,7 +83,7 @@ func TestRouterDispatch_DeleteServiceOverride(t *testing.T) { // would be called, so a missing-component error would be returned instead. func TestRouterDispatch_DeleteAccount_RoutesCorrectly(t *testing.T) { ctx := context.Background() - r := setupRouterForDispatch(ctx) + r := setupRouterForDispatch(t, ctx) accountID := "11111111-1111-1111-1111-111111111111" req, method, path := routerReq("DELETE", "/api/accounts/"+accountID, "") @@ -96,7 +99,7 @@ func TestRouterDispatch_DeleteAccount_RoutesCorrectly(t *testing.T) { // (non-error) result proves the more-specific exact-path route won. (#949/#951). func TestRouterDispatch_AccountsList_RoutesToMinimalHandler(t *testing.T) { ctx := context.Background() - r := setupRouterForDispatch(ctx) + r := setupRouterForDispatch(t, ctx) req, method, path := routerReq("GET", "/api/accounts/list", "") result, err := r.Route(ctx, method, path, req) @@ -113,7 +116,7 @@ func TestRouterDispatch_AccountsList_RoutesToMinimalHandler(t *testing.T) { // would receive id="uuid/accounts", which contains a slash → "invalid ID" error. func TestRouterDispatch_PlanAccountsGET_CorrectDispatch(t *testing.T) { ctx := context.Background() - r := setupRouterForDispatch(ctx) + r := setupRouterForDispatch(t, ctx) planID := "22222222-2222-2222-2222-222222222222" req, method, path := routerReq("GET", "/api/plans/"+planID+"/accounts", "") @@ -125,7 +128,7 @@ func TestRouterDispatch_PlanAccountsGET_CorrectDispatch(t *testing.T) { func TestRouterDispatch_PlanAccountsPUT_CorrectDispatch(t *testing.T) { ctx := context.Background() - r := setupRouterForDispatch(ctx) + r := setupRouterForDispatch(t, ctx) planID := "22222222-2222-2222-2222-222222222222" // Body uses a single valid UUID — universal-plans fix rejects empty diff --git a/internal/api/handler_accounts_test.go b/internal/api/handler_accounts_test.go index 52f418a59..beeb0362e 100644 --- a/internal/api/handler_accounts_test.go +++ b/internal/api/handler_accounts_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "testing" "github.com/LeanerCloud/cloud-commitments-go/pkg/logging" @@ -17,6 +18,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -54,6 +56,18 @@ func setupAdminMock(ctx context.Context) *MockConfigStore { return new(MockConfigStore) } +// relaxUnreachedStubs marks the expectations a shared fixture registered for +// methods as optional, for a test whose path never reaches those calls; the +// test asserts what does or does not happen itself. Times(0) also clears a +// .Once() count, which testify would otherwise still report as unmet. +func relaxUnreachedStubs(m *mock.Mock, methods ...string) { + for _, c := range m.ExpectedCalls { + if slices.Contains(methods, c.Method) { + c.Maybe().Times(0) + } + } +} + func setupAdminAuth(ctx context.Context, mockAuth *MockAuthService) { mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminAccountSession(), nil) mockAuth.grantAdmin() @@ -64,9 +78,11 @@ func setupAdminAuth(ctx context.Context, mockAuth *MockAuthService) { func TestListAccounts_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) // MockConfigStore.ListCloudAccounts is a stub returning nil — override with embedded mock accounts := []config.CloudAccount{sampleAccount()} customStore := &mockConfigStoreAccounts{ @@ -87,9 +103,11 @@ func TestListAccounts_Success(t *testing.T) { func TestListAccounts_ReturnsEmptySlice(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} result, err := handler.listAccounts(ctx, adminRequest("")) @@ -139,6 +157,7 @@ func standardRequest(body string) *events.LambdaFunctionURLRequest { //nolint:un func TestListAccountsMinimal_StandardUserAllowed_NoSensitiveFields(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupStandardUserAuth(ctx, mockAuth, "view", "recommendations", true, []string{"*"}) full := sampleAccount() @@ -182,6 +201,7 @@ func TestListAccountsMinimal_StandardUserAllowed_NoSensitiveFields(t *testing.T) func TestListAccounts_StandardUserDenied_403(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupStandardUserAuth(ctx, mockAuth, "view", "accounts", false, nil) handler := &Handler{auth: mockAuth, config: setupAdminMock(ctx)} @@ -198,6 +218,7 @@ func TestListAccounts_StandardUserDenied_403(t *testing.T) { func TestListAccountsMinimal_NoViewRecommendations_403(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupStandardUserAuth(ctx, mockAuth, "view", "recommendations", false, nil) handler := &Handler{auth: mockAuth, config: setupAdminMock(ctx)} @@ -214,6 +235,7 @@ func TestListAccountsMinimal_NoViewRecommendations_403(t *testing.T) { func TestListAccountsMinimal_ScopesByAllowedAccounts(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupStandardUserAuth(ctx, mockAuth, "view", "recommendations", true, []string{sampleAccount().ID}) prod := sampleAccount() @@ -239,6 +261,7 @@ func TestListAccountsMinimal_ScopesByAllowedAccounts(t *testing.T) { func TestListAccountsMinimal_ReturnsEmptySlice(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupStandardUserAuth(ctx, mockAuth, "view", "recommendations", true, []string{"*"}) handler := &Handler{auth: mockAuth, config: setupAdminMock(ctx)} @@ -254,9 +277,11 @@ func TestListAccountsMinimal_ReturnsEmptySlice(t *testing.T) { func TestCreateAccount_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012"}` @@ -272,9 +297,11 @@ func TestCreateAccount_Success(t *testing.T) { func TestCreateAccount_MissingName(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"provider":"aws","external_id":"123456789012"}` @@ -290,9 +317,11 @@ func TestCreateAccount_MissingName(t *testing.T) { func TestCreateAccount_InvalidProvider(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"digitalocean","external_id":"123"}` @@ -308,9 +337,11 @@ func TestCreateAccount_InvalidProvider(t *testing.T) { func TestCreateAccount_MissingExternalID(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws"}` @@ -326,9 +357,11 @@ func TestCreateAccount_MissingExternalID(t *testing.T) { func TestCreateAccount_InvalidContactEmail(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012","contact_email":"not-an-email"}` @@ -344,9 +377,11 @@ func TestCreateAccount_InvalidContactEmail(t *testing.T) { func TestCreateAccount_EmptyContactEmail(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} // Empty contact_email is allowed (field is optional). @@ -363,9 +398,11 @@ func TestCreateAccount_EmptyContactEmail(t *testing.T) { func TestCreateAccount_RejectsTLDlessContactEmail(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012","contact_email":"admin@intranet"}` @@ -383,9 +420,11 @@ func TestCreateAccount_RejectsTLDlessContactEmail(t *testing.T) { func TestCreateAccount_AcceptsValidContactEmail(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"name":"Acme","provider":"aws","external_id":"123456789012","contact_email":"admin@example.com"}` @@ -400,6 +439,7 @@ func TestCreateAccount_AcceptsValidContactEmail(t *testing.T) { func TestGetAccount_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) acct := sampleAccount() @@ -419,9 +459,11 @@ func TestGetAccount_Success(t *testing.T) { func TestGetAccount_NotFound(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return nil, nil } handler := &Handler{auth: mockAuth, config: store} @@ -434,9 +476,13 @@ func TestGetAccount_NotFound(t *testing.T) { func TestGetAccount_InvalidUUID(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) + // The id is refused before the session is read. + relaxUnreachedStubs(&mockAuth.Mock, "ValidateSession") store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} result, err := handler.getAccount(ctx, adminRequest(""), "not-a-uuid") @@ -449,9 +495,11 @@ func TestGetAccount_InvalidUUID(t *testing.T) { func TestDeleteAccount_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} result, err := handler.deleteAccount(ctx, adminRequest(""), "11111111-1111-1111-1111-111111111111") @@ -469,9 +517,11 @@ func TestDeleteAccount_Success(t *testing.T) { func TestDeleteAccount_PendingExecutions_Returns409(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 3, nil } @@ -508,9 +558,11 @@ func TestDeleteAccount_PendingExecutions_Returns409(t *testing.T) { func TestDeleteAccount_PendingZero_DeletesNormally(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) countCalled := false store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { countCalled = true @@ -538,9 +590,11 @@ func TestDeleteAccount_PendingZero_DeletesNormally(t *testing.T) { func TestDeleteAccount_ActiveExecutions_Returns409(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 0, nil } var liveAccount string store.CountLiveExecutionsForAccountFn = func(_ context.Context, id string) (int, error) { @@ -577,9 +631,11 @@ func TestDeleteAccount_ActiveExecutions_Returns409(t *testing.T) { func TestDeleteAccount_PendingTakesPrecedenceOverActive(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 1, nil } store.ListPendingExecutionIDsForAccountFn = func(_ context.Context, _ string) ([]string, error) { return []string{"exec-1"}, nil @@ -605,9 +661,11 @@ func TestDeleteAccount_PendingTakesPrecedenceOverActive(t *testing.T) { func TestDeleteAccount_OnlyTerminalExecutions_Deletes(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 0, nil } liveCalled := false store.CountLiveExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { @@ -633,9 +691,11 @@ func TestDeleteAccount_OnlyTerminalExecutions_Deletes(t *testing.T) { func TestDeleteAccount_LiveCountErr_Returns500(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 0, nil } store.CountLiveExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 0, errors.New("db down") @@ -662,9 +722,11 @@ func TestDeleteAccount_LiveCountErr_Returns500(t *testing.T) { func TestDeleteAccount_PendingListErr_StillReturns409(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 5, nil } @@ -696,9 +758,11 @@ func TestDeleteAccount_PendingListErr_StillReturns409(t *testing.T) { func TestDeleteAccount_FKViolationRace_Returns409(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) // Preflight count is 0 (no pending rows at preflight time). store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 0, nil @@ -742,9 +806,11 @@ func TestDeleteAccount_FKViolationRace_RequeriesPreflight(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) pendingCalls, liveCalls := 0, 0 // First call is the preflight (nothing yet); the second is the // re-query after the FK violation, which sees the raced-in row. @@ -786,9 +852,11 @@ func TestDeleteAccount_FKViolationRace_RequeriesPreflight(t *testing.T) { func TestDeleteAccount_FKViolationRace_RequeryErrorFallsBack(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) calls := 0 store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { calls++ @@ -816,9 +884,11 @@ func TestDeleteAccount_FKViolationRace_RequeryErrorFallsBack(t *testing.T) { func TestDeleteAccount_NonFKError_StillReturns500(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.CountPendingExecutionsForAccountFn = func(_ context.Context, _ string) (int, error) { return 0, nil } @@ -839,9 +909,11 @@ func TestDeleteAccount_NonFKError_StillReturns500(t *testing.T) { func TestDeleteAccount_NotFound(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return nil, nil } deleteCalled := false store.DeleteCloudAccountFn = func(_ context.Context, _ string) error { @@ -862,9 +934,11 @@ func TestDeleteAccount_NotFound(t *testing.T) { func TestSaveAccountCredentials_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store, credStore: &MockCredentialStore{}} body := `{"credential_type":"aws_access_keys","payload":{"access_key_id":"AKIA...","secret_access_key":"secret"}}` @@ -876,9 +950,11 @@ func TestSaveAccountCredentials_Success(t *testing.T) { func TestSaveAccountCredentials_InvalidType(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"credential_type":"unknown_type","payload":{}}` @@ -893,9 +969,11 @@ func TestSaveAccountCredentials_InvalidType(t *testing.T) { func TestSaveAccountCredentials_InvalidJSON(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} result, err := handler.saveAccountCredentials(ctx, adminRequest("{invalid}"), "11111111-1111-1111-1111-111111111111") @@ -912,9 +990,11 @@ func TestSaveAccountCredentials_InvalidJSON(t *testing.T) { func TestSaveAccountCredentials_NotFound_WithNilCredStore(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return nil, nil } handler := &Handler{auth: mockAuth, config: store, credStore: nil} @@ -930,6 +1010,7 @@ func TestSaveAccountCredentials_NotFound_WithNilCredStore(t *testing.T) { func TestListAccountServiceOverrides_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) overrides := []config.AccountServiceOverride{ @@ -962,9 +1043,11 @@ func TestListAccountServiceOverrides_Success(t *testing.T) { func TestSaveAccountServiceOverride_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"enabled":true,"term":1}` @@ -983,9 +1066,11 @@ func TestSaveAccountServiceOverride_Success(t *testing.T) { func TestSaveAccountServiceOverride_InvalidCombo_Returns400(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) saveCalled := false store.SaveAccountServiceOverrideFn = func(_ context.Context, _ *config.AccountServiceOverride) error { saveCalled = true @@ -1021,9 +1106,11 @@ func TestSaveAccountServiceOverride_InvalidCombo_Returns400(t *testing.T) { func TestSaveAccountServiceOverride_ValidCombo_Saves(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{ auth: mockAuth, config: store, @@ -1053,9 +1140,11 @@ func TestSaveAccountServiceOverride_NoProbeData_Permissive(t *testing.T) { t.Run("nil commitmentOpts", func(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} // commitmentOpts is nil result, err := handler.saveAccountServiceOverride(ctx, adminRequest(body), path) @@ -1065,9 +1154,11 @@ func TestSaveAccountServiceOverride_NoProbeData_Permissive(t *testing.T) { t.Run("ErrNoData falls through permissive", func(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{ auth: mockAuth, config: store, @@ -1145,9 +1236,11 @@ func TestParseServiceOverridePath_Invalid(t *testing.T) { func TestSetPlanAccounts_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, config: store} body := `{"account_ids":["11111111-1111-1111-1111-111111111111"]}` @@ -1185,10 +1278,12 @@ func awsPlan209() *config.PurchasePlan { func TestSetPlanAccounts_SingleMismatch(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -1216,10 +1311,12 @@ func TestSetPlanAccounts_SingleMismatch(t *testing.T) { func TestSetPlanAccounts_MultipleMismatches(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -1256,10 +1353,12 @@ func TestSetPlanAccounts_MultipleMismatches(t *testing.T) { func TestSetPlanAccounts_ValidHappyPath(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) var capturedIDs []string store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -1282,9 +1381,11 @@ func TestSetPlanAccounts_ValidHappyPath(t *testing.T) { func TestSetPlanAccounts_StoreNotFoundAfterValidationMapsTo404(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -1308,10 +1409,12 @@ func TestSetPlanAccounts_StoreNotFoundAfterValidationMapsTo404(t *testing.T) { func TestSetPlanAccounts_PlanNotFound(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return nil, nil // store-style "not found": (nil, nil) } @@ -1334,10 +1437,12 @@ func TestSetPlanAccounts_PlanNotFound(t *testing.T) { func TestSetPlanAccounts_AccountNotFound(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -1366,10 +1471,12 @@ func TestSetPlanAccounts_AccountNotFound(t *testing.T) { func TestSetPlanAccounts_MixedValidAndMismatch(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -1404,10 +1511,12 @@ func TestSetPlanAccounts_MixedValidAndMismatch(t *testing.T) { func TestSetPlanAccounts_EmptyServicesSkipsValidation(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) var capturedIDs []string store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) // Plan with an empty services map — derived provider set is empty; // the validation block skips and the assignment passes through. // Pins the defensive behavior so a future change is conscious. @@ -1437,10 +1546,12 @@ func TestSetPlanAccounts_EmptyServicesSkipsValidation(t *testing.T) { func TestSetPlanAccounts_ColonKeyedPlanStillChecksProvider(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return &config.PurchasePlan{ID: planID209, Name: "sp-plan", Services: map[string]config.ServiceConfig{ "aws:savings-plans-compute": {Provider: "aws", Service: "savings-plans"}, @@ -1482,9 +1593,11 @@ func TestSetPlanAccounts_LegacyKeyedPlanUsesProviderField(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return &config.PurchasePlan{ID: planID209, Name: "alias plan", Services: map[string]config.ServiceConfig{ "original-key": {Provider: "azure", Service: "vm", Enabled: true}, @@ -1523,10 +1636,12 @@ func TestSetPlanAccounts_LegacyKeyedPlanUsesProviderField(t *testing.T) { func TestSetPlanAccounts_UnparseableServiceKeyFailsClosed(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return &config.PurchasePlan{ID: planID209, Name: "bad-key", Services: map[string]config.ServiceConfig{ "ec2": {Service: "ec2"}, @@ -1559,10 +1674,12 @@ func TestSetPlanAccounts_UnparseableServiceKeyFailsClosed(t *testing.T) { func TestSetPlanAccounts_RejectsEmpty(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) setCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.SetPlanAccountsFn = func(_ context.Context, _ string, _ []string) error { setCalled = true return nil @@ -1582,6 +1699,7 @@ func TestSetPlanAccounts_RejectsEmpty(t *testing.T) { func TestListPlanAccounts_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) accounts := []config.CloudAccount{sampleAccount()} @@ -1619,6 +1737,7 @@ func orgRootAccount() *config.CloudAccount { func TestDiscoverOrgAccounts_RejectsInvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) handler := &Handler{auth: mockAuth, config: setupAdminMock(ctx)} @@ -1632,6 +1751,7 @@ func TestDiscoverOrgAccounts_RejectsInvalidBody(t *testing.T) { func TestDiscoverOrgAccounts_RejectsInvalidAccountID(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) handler := &Handler{auth: mockAuth, config: setupAdminMock(ctx)} @@ -1645,8 +1765,10 @@ func TestDiscoverOrgAccounts_RejectsInvalidAccountID(t *testing.T) { func TestDiscoverOrgAccounts_RejectsNonAWSAccount(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, id string) (*config.CloudAccount, error) { return &config.CloudAccount{ID: id, Provider: "azure", AWSIsOrgRoot: true}, nil } @@ -1663,8 +1785,10 @@ func TestDiscoverOrgAccounts_RejectsNonAWSAccount(t *testing.T) { func TestDiscoverOrgAccounts_RejectsNonOrgRoot(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, id string) (*config.CloudAccount, error) { return &config.CloudAccount{ID: id, Provider: "aws", AWSIsOrgRoot: false}, nil } @@ -1681,8 +1805,10 @@ func TestDiscoverOrgAccounts_RejectsNonOrgRoot(t *testing.T) { func TestDiscoverOrgAccounts_NotFound(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return nil, nil } @@ -1703,6 +1829,7 @@ func TestDiscoverOrgAccounts_NotFound(t *testing.T) { func TestDiscoverOrgAccounts_RejectsNonAdmin(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) // Non-admin session: the user is not an Administrators-group member, so // HasPermissionAPI(admin,*) returns false and requireAdmin // (middleware.go) rejects with 403 (issue #907 group-only authz). @@ -1735,10 +1862,12 @@ func TestDiscoverOrgAccounts_RejectsNonAdmin(t *testing.T) { func TestDiscoverOrgAccounts_CredResolutionFailureIs5xx(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) root := orgRootAccount() store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return root, nil } @@ -1790,6 +1919,7 @@ func (e *errCredStore) DecryptPayload(_ string) ([]byte, error) func TestDiscoverOrgAccounts_HappyPathDedupesAndPersists(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) root := orgRootAccount() @@ -1807,6 +1937,7 @@ func TestDiscoverOrgAccounts_HappyPathDedupesAndPersists(t *testing.T) { } store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return root, nil } @@ -1872,6 +2003,7 @@ func TestDiscoverOrgAccounts_HappyPathDedupesAndPersists(t *testing.T) { func TestDiscoverOrgAccounts_SkipsDuplicateKeyOnInsert(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) root := &config.CloudAccount{ @@ -1884,6 +2016,7 @@ func TestDiscoverOrgAccounts_SkipsDuplicateKeyOnInsert(t *testing.T) { } store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return root, nil } @@ -1935,6 +2068,7 @@ func TestDiscoverOrgAccounts_SkipsDuplicateKeyOnInsert(t *testing.T) { func TestDiscoverOrgAccounts_AllowsNilDiscoveryResult(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) root := &config.CloudAccount{ @@ -1947,6 +2081,7 @@ func TestDiscoverOrgAccounts_AllowsNilDiscoveryResult(t *testing.T) { } store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return root, nil } @@ -2066,6 +2201,7 @@ const outOfScopeID = "22222222-2222-2222-2222-222222222222" func TestGetAccount_OutOfScope_Returns404(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupScopedAuth(ctx, mockAuth, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "view", "accounts", []string{"Production"}) acct := sampleAccount() @@ -2086,6 +2222,7 @@ func TestGetAccount_OutOfScope_Returns404(t *testing.T) { func TestGetAccount_InScope_ByID(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) // allowed_accounts holds acct.ID; entries match by account ID only (#1034). setupScopedAuth(ctx, mockAuth, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "view", "accounts", []string{"11111111-1111-1111-1111-111111111111"}) @@ -2111,6 +2248,7 @@ func TestGetAccount_InScope_ByID(t *testing.T) { func TestGetAccount_NameEntry_Returns404(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupScopedAuth(ctx, mockAuth, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "view", "accounts", []string{"Production"}) acct := sampleAccount() @@ -2131,6 +2269,7 @@ func TestGetAccount_NameEntry_Returns404(t *testing.T) { func TestDeleteAccount_OutOfScope_Returns404_NoDelete(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupScopedAuth(ctx, mockAuth, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "delete", "accounts", []string{"Production"}) acct := sampleAccount() @@ -2138,6 +2277,7 @@ func TestDeleteAccount_OutOfScope_Returns404_NoDelete(t *testing.T) { acct.Name = "Staging" deleteCalled := false store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return &acct, nil } store.DeleteCloudAccountFn = func(_ context.Context, _ string) error { deleteCalled = true @@ -2155,12 +2295,14 @@ func TestDeleteAccount_OutOfScope_Returns404_NoDelete(t *testing.T) { func TestSaveAccountCredentials_OutOfScope_Returns404(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupScopedAuth(ctx, mockAuth, "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "update", "accounts", []string{"Production"}) acct := sampleAccount() acct.ID = outOfScopeID acct.Name = "Staging" store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetCloudAccountFn = func(_ context.Context, _ string) (*config.CloudAccount, error) { return &acct, nil } handler := &Handler{auth: mockAuth, config: store, credStore: &MockCredentialStore{}} @@ -2212,6 +2354,7 @@ func (m *mockConfigStoreAccounts) UpdateCloudAccount(ctx context.Context, a *con func TestCreateAccount_DuplicateKey_Returns409(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) customStore := &mockConfigStoreAccounts{ @@ -2235,6 +2378,7 @@ func TestCreateAccount_DuplicateKey_Returns409(t *testing.T) { func TestUpdateAccount_DuplicateKey_Returns409(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) // updateAccount loads the existing account first; wire a valid getResult. @@ -2300,6 +2444,7 @@ func (m *mockConfigStoreAccounts) WithTx(_ context.Context, fn func(tx pgx.Tx) e func TestTestAccountCredentials_RequiresUpdateAccounts(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) viewOnlySession := &Session{ UserID: "dddddddd-dddd-dddd-dddd-dddddddddddd", @@ -2352,6 +2497,7 @@ func TestValidatePlanAccountProviders_GetAccountDBError_NoPIILeak(t *testing.T) const rawDBErr = "pq: connection refused to host db-internal-99.example.com" store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -2390,6 +2536,7 @@ func TestValidatePlanAccountProviders_AccountNotFound_Still404(t *testing.T) { ctx := context.Background() store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return awsPlan209(), nil } @@ -2422,6 +2569,7 @@ func TestGetPlanForAccountProviderValidation_GetPlanDBError_NoPIILeak(t *testing const rawDBErr = "pq: SSL connection has been closed unexpectedly by host db-plans-99.example.com" store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return nil, errors.New(rawDBErr) } @@ -2454,6 +2602,7 @@ func TestGetPlanForAccountProviderValidation_PlanNotFound_Still404(t *testing.T) ctx := context.Background() store := setupAdminMock(ctx) + t.Cleanup(func() { store.AssertExpectations(t) }) store.GetPurchasePlanFn = func(_ context.Context, _ string) (*config.PurchasePlan, error) { return nil, config.ErrNotFound } diff --git a/internal/api/handler_accounts_update_keep_test.go b/internal/api/handler_accounts_update_keep_test.go index 65c230e34..38a20fe99 100644 --- a/internal/api/handler_accounts_update_keep_test.go +++ b/internal/api/handler_accounts_update_keep_test.go @@ -51,6 +51,7 @@ func putAccountFlags(t *testing.T, existingEnabled, existingOrgRoot bool, extra t.Helper() ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) existing := sampleAccount() @@ -121,6 +122,7 @@ func TestCreateAccount_FlagDefaultsUnchanged(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) setupAdminAuth(ctx, mockAuth) store := newAccountWriteCapture(ctx, nil) handler := &Handler{auth: mockAuth, config: store} diff --git a/internal/api/handler_analytics_invalid_params_test.go b/internal/api/handler_analytics_invalid_params_test.go index 68549faa2..8d3307b10 100644 --- a/internal/api/handler_analytics_invalid_params_test.go +++ b/internal/api/handler_analytics_invalid_params_test.go @@ -20,12 +20,14 @@ func TestHandler_getHistoryAnalytics_UnsupportedIntervalIs400(t *testing.T) { t.Run(interval, func(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) // What the Postgres client answers for an interval outside its // allowlist, so the pre-fix handler fails this test with a 500 // instead of panicking on an unexpected call. mockClient.On("QueryHistory", ctx, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, interval). Return(nil, nil, fmt.Errorf("unsupported interval %q", interval)).Maybe() mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} _, err := handler.getHistoryAnalytics(ctx, req, map[string]string{"interval": interval}) @@ -47,6 +49,7 @@ func TestHandler_getHistoryAnalytics_SupportedIntervalsReachStore(t *testing.T) mockClient.On("QueryHistory", ctx, []string(nil), map[string][]string(nil), "", mock.Anything, mock.Anything, interval). Return([]HistoryDataPoint{}, (*HistorySummary)(nil), nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} result, err := handler.getHistoryAnalytics(ctx, req, map[string]string{"interval": interval}) @@ -64,10 +67,12 @@ func TestHandler_getHistoryBreakdown_UnsupportedDimensionIs400(t *testing.T) { t.Run(dimension, func(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) // The Postgres client's answer for an unknown dimension; see above. mockClient.On("QueryBreakdown", ctx, mock.Anything, mock.Anything, mock.Anything, mock.Anything, dimension). Return(nil, fmt.Errorf("unsupported dimension %q", dimension)).Maybe() mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} _, err := handler.getHistoryBreakdown(ctx, req, map[string]string{"dimension": dimension}) @@ -89,6 +94,7 @@ func TestHandler_getHistoryBreakdown_SupportedDimensionsReachStore(t *testing.T) mockClient.On("QueryBreakdown", ctx, []string(nil), map[string][]string(nil), mock.Anything, mock.Anything, dimension). Return(map[string]BreakdownValue{}, nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} result, err := handler.getHistoryBreakdown(ctx, req, map[string]string{"dimension": dimension}) diff --git a/internal/api/handler_analytics_scope_provider_test.go b/internal/api/handler_analytics_scope_provider_test.go index 3e9bcf007..e160b961d 100644 --- a/internal/api/handler_analytics_scope_provider_test.go +++ b/internal/api/handler_analytics_scope_provider_test.go @@ -35,24 +35,33 @@ func analyticsScopeCallPath(t *testing.T, path string, scope []string, accountID func analyticsParamsCall(t *testing.T, path string, scope []string, params map[string]string, accounts []config.CloudAccount) (status int, uuids []string, externals map[string][]string) { t.Helper() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, "tok").Return(&Session{UserID: "u-1", Email: "u@example.com"}, nil) mockAuth.On("HasPermissionAPI", mock.Anything, "u-1", mock.Anything, mock.Anything).Return(true, nil) - mockAuth.On("GetAllowedAccountsAPI", mock.Anything, "u-1").Return(scope, nil) + // A malformed account id is refused before the allow-list is read; a + // served request must have read it (checked below). + mockAuth.On("GetAllowedAccountsAPI", mock.Anything, "u-1").Return(scope, nil).Maybe() + // Each endpoint runs only its own queries and a refused request runs + // none, so the query stubs are optional here; a served request is checked + // against analyticsPathQueries below. client := new(MockAnalyticsClient) + t.Cleanup(func() { client.AssertExpectations(t) }) client.On("QueryHistory", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return([]HistoryDataPoint{}, &HistorySummary{}, nil) + Return([]HistoryDataPoint{}, &HistorySummary{}, nil).Maybe() client.On("QueryBreakdown", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(map[string]BreakdownValue{}, nil) + Return(map[string]BreakdownValue{}, nil).Maybe() snaps := new(MockAnalyticsSnapshotStore) + t.Cleanup(func() { snaps.AssertExpectations(t) }) snaps.On("QueryMonthlyTotals", mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return([]analytics.MonthlySummary{}, nil) + Return([]analytics.MonthlySummary{}, nil).Maybe() snaps.On("QueryByProvider", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return([]analytics.ProviderBreakdown{}, nil) + Return([]analytics.ProviderBreakdown{}, nil).Maybe() snaps.On("QueryByService", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return([]analytics.ServiceBreakdown{}, nil) + Return([]analytics.ServiceBreakdown{}, nil).Maybe() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.ListCloudAccountsFn = func(_ context.Context, _ config.CloudAccountFilter) ([]config.CloudAccount, error) { return accounts, nil } @@ -76,12 +85,28 @@ func analyticsParamsCall(t *testing.T, path string, scope []string, params map[s } uuids, externals = u, e } + if resp.StatusCode == 200 { + mockAuth.AssertCalled(t, "GetAllowedAccountsAPI", mock.Anything, "u-1") + ran := make([]string, 0, len(calls)) + for _, c := range calls { + ran = append(ran, c.Method) + } + assert.ElementsMatch(t, analyticsPathQueries[path], ran, "a served %s runs each of its queries once", path) + } return resp.StatusCode, uuids, externals } // analyticsPaths are the endpoints that run resolveAnalyticsAccountFilter. var analyticsPaths = []string{"/api/history/analytics", "/api/history/breakdown", "/api/analytics/trends"} +// analyticsPathQueries are the queries each of analyticsPaths runs once it +// serves a request. +var analyticsPathQueries = map[string][]string{ + "/api/history/analytics": {"QueryHistory"}, + "/api/history/breakdown": {"QueryBreakdown"}, + "/api/analytics/trends": {"QueryMonthlyTotals", "QueryByProvider", "QueryByService"}, +} + // Issue #520: a raw external id shared by accounts of different providers // cannot be attributed to one of them. The scope decision used to read the // account name from a map keyed by external id alone (last listed account @@ -187,14 +212,17 @@ func TestHandleRequest_Analytics_UnregisteredRawID(t *testing.T) { func TestHandleRequest_Analytics_AccountListFailureIsAnError(t *testing.T) { t.Parallel() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, "tok").Return(&Session{UserID: "u-1", Email: "u@example.com"}, nil) mockAuth.On("HasPermissionAPI", mock.Anything, "u-1", mock.Anything, mock.Anything).Return(true, nil) mockAuth.On("GetAllowedAccountsAPI", mock.Anything, "u-1").Return([]string{"999"}, nil) store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.ListCloudAccountsFn = func(_ context.Context, _ config.CloudAccountFilter) ([]config.CloudAccount, error) { return nil, assert.AnError } client := new(MockAnalyticsClient) + t.Cleanup(func() { client.AssertExpectations(t) }) h := &Handler{auth: mockAuth, analyticsClient: client, config: store} resp, _ := h.HandleRequest(context.Background(), &events.LambdaFunctionURLRequest{ diff --git a/internal/api/handler_analytics_test.go b/internal/api/handler_analytics_test.go index 0eef5dbc5..23e59dcc0 100644 --- a/internal/api/handler_analytics_test.go +++ b/internal/api/handler_analytics_test.go @@ -50,15 +50,18 @@ func TestHandler_getHistoryAnalytics_ExternalIDOnlyAccount(t *testing.T) { accountExternal := "999988887777" mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockClient.On("QueryHistory", ctx, []string{accountUUID}, map[string][]string{"aws": {accountExternal}}, "", mock.Anything, mock.Anything, "hourly"). Return([]HistoryDataPoint{{TotalSavings: 50.0, PurchaseCount: 1}}, &HistorySummary{TotalPurchases: 1}, nil) mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.ListCloudAccountsFn = func(_ context.Context, _ config.CloudAccountFilter) ([]config.CloudAccount, error) { return []config.CloudAccount{{ID: accountUUID, Name: "Account B", Provider: "aws", ExternalID: accountExternal}}, nil } mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: mockStore} result, err := handler.getHistoryAnalytics(ctx, req, map[string]string{"account_id": accountUUID}) @@ -95,6 +98,7 @@ func adminAnalyticsReq(ctx context.Context) (*MockAuthService, *events.LambdaFun func TestHandler_getHistoryAnalytics_Success(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) dataPoints := []HistoryDataPoint{ {Timestamp: time.Now(), TotalSavings: 100.0, PurchaseCount: 5}, @@ -108,6 +112,7 @@ func TestHandler_getHistoryAnalytics_Success(t *testing.T) { mockClient.On("QueryHistory", ctx, []string(nil), map[string][]string{"": {"account-123"}}, "", mock.Anything, mock.Anything, "hourly").Return(dataPoints, summary, nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} params := map[string]string{ @@ -128,6 +133,7 @@ func TestHandler_getHistoryAnalytics_Success(t *testing.T) { func TestHandler_getHistoryAnalytics_NoClient(t *testing.T) { ctx := context.Background() mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} _, err := handler.getHistoryAnalytics(ctx, req, map[string]string{}) @@ -138,12 +144,14 @@ func TestHandler_getHistoryAnalytics_NoClient(t *testing.T) { func TestHandler_getHistoryAnalytics_DefaultInterval(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) dataPoints := []HistoryDataPoint{} // Empty account_id: resolver returns no UUIDs and no external ids. mockClient.On("QueryHistory", ctx, []string(nil), map[string][]string(nil), "", mock.Anything, mock.Anything, "hourly").Return(dataPoints, (*HistorySummary)(nil), nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} params := map[string]string{} // No interval specified @@ -162,10 +170,12 @@ func TestHandler_getHistoryAnalytics_DefaultInterval(t *testing.T) { func TestHandler_getHistoryAnalytics_ProviderFilter(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockClient.On("QueryHistory", ctx, []string(nil), map[string][]string(nil), "aws", mock.Anything, mock.Anything, "hourly"). Return([]HistoryDataPoint{}, (*HistorySummary)(nil), nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} _, err := handler.getHistoryAnalytics(ctx, req, map[string]string{"provider": "aws"}) @@ -179,10 +189,12 @@ func TestHandler_getHistoryAnalytics_ProviderFilter(t *testing.T) { func TestHandler_getHistoryAnalytics_ProviderAllIsNoFilter(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockClient.On("QueryHistory", ctx, []string(nil), map[string][]string(nil), "", mock.Anything, mock.Anything, "hourly"). Return([]HistoryDataPoint{}, (*HistorySummary)(nil), nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} _, err := handler.getHistoryAnalytics(ctx, req, map[string]string{"provider": "all"}) @@ -195,8 +207,10 @@ func TestHandler_getHistoryAnalytics_ProviderAllIsNoFilter(t *testing.T) { func TestHandler_getHistoryAnalytics_InvalidProvider(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} _, err := handler.getHistoryAnalytics(ctx, req, map[string]string{"provider": "oracle"}) @@ -208,8 +222,10 @@ func TestHandler_getHistoryAnalytics_InvalidProvider(t *testing.T) { func TestHandler_getHistoryAnalytics_InvalidDateRange(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient} params := map[string]string{ @@ -224,10 +240,12 @@ func TestHandler_getHistoryAnalytics_InvalidDateRange(t *testing.T) { func TestHandler_getHistoryAnalytics_QueryError(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockClient.On("QueryHistory", ctx, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil, errors.New("query failed")) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} params := map[string]string{} @@ -243,7 +261,9 @@ func TestHandler_getHistoryAnalytics_QueryError(t *testing.T) { func TestHandler_getHistoryAnalytics_ScopedUser_RequiresAccountID(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "viewer-token").Return(&Session{ UserID: "viewer-1", }, nil) @@ -263,6 +283,7 @@ func TestHandler_getHistoryAnalytics_ScopedUser_RequiresAccountID(t *testing.T) func TestHandler_getHistoryBreakdown_Success(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) breakdownData := map[string]BreakdownValue{ "rds": {PurchaseCount: 10, TotalSavings: 500.0}, @@ -273,6 +294,7 @@ func TestHandler_getHistoryBreakdown_Success(t *testing.T) { mockClient.On("QueryBreakdown", ctx, []string(nil), map[string][]string{"": {"account-123"}}, mock.Anything, mock.Anything, "service").Return(breakdownData, nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} params := map[string]string{ @@ -293,6 +315,7 @@ func TestHandler_getHistoryBreakdown_Success(t *testing.T) { func TestHandler_getHistoryBreakdown_NoClient(t *testing.T) { ctx := context.Background() mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} _, err := handler.getHistoryBreakdown(ctx, req, map[string]string{}) @@ -303,11 +326,13 @@ func TestHandler_getHistoryBreakdown_NoClient(t *testing.T) { func TestHandler_getHistoryBreakdown_DefaultDimension(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) breakdownData := map[string]BreakdownValue{} mockClient.On("QueryBreakdown", ctx, []string(nil), map[string][]string(nil), mock.Anything, mock.Anything, "service").Return(breakdownData, nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} params := map[string]string{} // No dimension specified @@ -321,8 +346,10 @@ func TestHandler_getHistoryBreakdown_DefaultDimension(t *testing.T) { func TestHandler_getHistoryBreakdown_InvalidDateRange(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient} params := map[string]string{ @@ -337,10 +364,12 @@ func TestHandler_getHistoryBreakdown_InvalidDateRange(t *testing.T) { func TestHandler_getHistoryBreakdown_QueryError(t *testing.T) { ctx := context.Background() mockClient := new(MockAnalyticsClient) + t.Cleanup(func() { mockClient.AssertExpectations(t) }) mockClient.On("QueryBreakdown", ctx, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("breakdown failed")) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsClient: mockClient, config: new(MockConfigStore)} params := map[string]string{} @@ -353,10 +382,12 @@ func TestHandler_getHistoryBreakdown_QueryError(t *testing.T) { func TestHandler_triggerAnalyticsCollection_Success(t *testing.T) { ctx := context.Background() mockCollector := new(MockAnalyticsCollector) + t.Cleanup(func() { mockCollector.AssertExpectations(t) }) mockCollector.On("Collect", ctx).Return(nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsCollector: mockCollector} result, err := handler.triggerAnalyticsCollection(ctx, req, nil) @@ -371,6 +402,7 @@ func TestHandler_triggerAnalyticsCollection_Success(t *testing.T) { func TestHandler_triggerAnalyticsCollection_NoCollector(t *testing.T) { ctx := context.Background() mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} _, err := handler.triggerAnalyticsCollection(ctx, req, nil) @@ -381,10 +413,12 @@ func TestHandler_triggerAnalyticsCollection_NoCollector(t *testing.T) { func TestHandler_triggerAnalyticsCollection_Error(t *testing.T) { ctx := context.Background() mockCollector := new(MockAnalyticsCollector) + t.Cleanup(func() { mockCollector.AssertExpectations(t) }) mockCollector.On("Collect", ctx).Return(errors.New("collection error")) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsCollector: mockCollector} _, err := handler.triggerAnalyticsCollection(ctx, req, nil) @@ -397,7 +431,9 @@ func TestHandler_triggerAnalyticsCollection_Error(t *testing.T) { func TestHandler_triggerAnalyticsCollection_NonAdmin(t *testing.T) { ctx := context.Background() mockCollector := new(MockAnalyticsCollector) + t.Cleanup(func() { mockCollector.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "user-token").Return(&Session{ UserID: "user-1", }, nil) @@ -591,11 +627,13 @@ func TestHandler_getAnalyticsTrends_Success_ScopesByAccount(t *testing.T) { t.Cleanup(func() { mockSnap.AssertExpectations(t) }) mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.ListCloudAccountsFn = func(_ context.Context, _ config.CloudAccountFilter) ([]config.CloudAccount, error) { return []config.CloudAccount{{ID: accountUUID, Name: "Account B", Provider: "aws", ExternalID: accountExternal}}, nil } mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsSnapshots: mockSnap, config: mockStore} result, err := handler.getAnalyticsTrends(ctx, req, map[string]string{"account_id": accountUUID}) @@ -614,6 +652,7 @@ func TestHandler_getAnalyticsTrends_MonthlyCommitmentAndUsage(t *testing.T) { ctx := context.Background() commitment, usage, zero := 720.0, 640.0, 0.0 mockSnap := new(MockAnalyticsSnapshotStore) + t.Cleanup(func() { mockSnap.AssertExpectations(t) }) mockSnap.On("QueryMonthlyTotals", ctx, mock.Anything, mock.Anything, mock.Anything). Return([]analytics.MonthlySummary{ {Provider: "aws", Service: "ec2", TotalSavings: 100, TotalCommitment: &commitment, TotalUsage: &usage}, @@ -626,6 +665,7 @@ func TestHandler_getAnalyticsTrends_MonthlyCommitmentAndUsage(t *testing.T) { Return([]analytics.ServiceBreakdown{}, nil) mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, analyticsSnapshots: mockSnap, config: new(MockConfigStore)} result, err := handler.getAnalyticsTrends(ctx, req, map[string]string{}) @@ -650,6 +690,7 @@ func TestHandler_getAnalyticsTrends_MonthlyCommitmentAndUsage(t *testing.T) { func TestHandler_getAnalyticsTrends_NoStore(t *testing.T) { ctx := context.Background() mockAuth, req := adminAnalyticsReq(ctx) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} _, err := handler.getAnalyticsTrends(ctx, req, map[string]string{}) @@ -664,7 +705,9 @@ func TestHandler_getAnalyticsTrends_NoStore(t *testing.T) { func TestHandler_getAnalyticsTrends_ScopedUser_RequiresAccountID(t *testing.T) { ctx := context.Background() mockSnap := new(MockAnalyticsSnapshotStore) + t.Cleanup(func() { mockSnap.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "viewer-token").Return(&Session{UserID: "viewer-1"}, nil) mockAuth.On("HasPermissionAPI", ctx, "viewer-1", "view", "purchases").Return(true, nil) mockAuth.On("GetAllowedAccountsAPI", ctx, "viewer-1").Return([]string{"Production"}, nil) @@ -683,12 +726,15 @@ func TestHandler_getAnalyticsTrends_ScopedUser_RequiresAccountID(t *testing.T) { func TestHandler_getAnalyticsTrends_ScopedUser_OutsideScope(t *testing.T) { ctx := context.Background() mockSnap := new(MockAnalyticsSnapshotStore) + t.Cleanup(func() { mockSnap.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "viewer-token").Return(&Session{UserID: "viewer-1"}, nil) mockAuth.On("HasPermissionAPI", ctx, "viewer-1", "view", "purchases").Return(true, nil) mockAuth.On("GetAllowedAccountsAPI", ctx, "viewer-1").Return([]string{"Production"}, nil) mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.ListCloudAccountsFn = func(_ context.Context, _ config.CloudAccountFilter) ([]config.CloudAccount, error) { return []config.CloudAccount{{ID: "other-acct", Name: "Staging", Provider: "aws", ExternalID: "111122223333"}}, nil } diff --git a/internal/api/handler_apikeys_test.go b/internal/api/handler_apikeys_test.go index 2757e0cc0..82db034f1 100644 --- a/internal/api/handler_apikeys_test.go +++ b/internal/api/handler_apikeys_test.go @@ -45,6 +45,7 @@ func (m *MockRateLimiter) AllowWithUser(ctx context.Context, userID, limitType s func TestHandler_listAPIKeys_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -86,6 +87,7 @@ func TestHandler_listAPIKeys_NoAuthService(t *testing.T) { func TestHandler_listAPIKeys_NoToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ @@ -100,6 +102,7 @@ func TestHandler_listAPIKeys_NoToken(t *testing.T) { func TestHandler_listAPIKeys_InvalidSession(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "invalid-token").Return(nil, errors.New("invalid session")) @@ -119,6 +122,7 @@ func TestHandler_listAPIKeys_InvalidSession(t *testing.T) { func TestHandler_listAPIKeys_ServiceError(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -141,7 +145,9 @@ func TestHandler_listAPIKeys_ServiceError(t *testing.T) { func TestHandler_createAPIKey_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockRateLimiter := new(MockRateLimiter) + t.Cleanup(func() { mockRateLimiter.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -179,6 +185,7 @@ func TestHandler_createAPIKey_NoAuthService(t *testing.T) { func TestHandler_createAPIKey_NoToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{} @@ -191,6 +198,7 @@ func TestHandler_createAPIKey_NoToken(t *testing.T) { func TestHandler_createAPIKey_InvalidSession(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "bad-token").Return(nil, errors.New("invalid session")) @@ -210,7 +218,9 @@ func TestHandler_createAPIKey_InvalidSession(t *testing.T) { func TestHandler_createAPIKey_RateLimited(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockRateLimiter := new(MockRateLimiter) + t.Cleanup(func() { mockRateLimiter.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -234,6 +244,7 @@ func TestHandler_createAPIKey_RateLimited(t *testing.T) { func TestHandler_createAPIKey_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -256,6 +267,7 @@ func TestHandler_createAPIKey_InvalidBody(t *testing.T) { func TestHandler_createAPIKey_ServiceError(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -293,7 +305,9 @@ func TestHandler_createAPIKey_ServiceError(t *testing.T) { func TestHandler_createAPIKey_WrongPassword(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockRateLimiter := new(MockRateLimiter) + t.Cleanup(func() { mockRateLimiter.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -327,6 +341,7 @@ func TestHandler_createAPIKey_PasswordDecodedBeforeForwarding(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) mockRateLimiter := new(MockRateLimiter) + t.Cleanup(func() { mockRateLimiter.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -361,7 +376,9 @@ func TestHandler_createAPIKey_PasswordDecodedBeforeForwarding(t *testing.T) { func TestHandler_createAPIKey_ExpiresAtRequired(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockRateLimiter := new(MockRateLimiter) + t.Cleanup(func() { mockRateLimiter.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -392,7 +409,9 @@ func TestHandler_createAPIKey_ExpiresAtRequired(t *testing.T) { func TestHandler_createAPIKey_ExpiresAtTooFar(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockRateLimiter := new(MockRateLimiter) + t.Cleanup(func() { mockRateLimiter.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -429,6 +448,7 @@ func TestHandler_createAPIKey_ExpiresAtTooFar(t *testing.T) { func TestHandler_deleteAPIKey_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -470,6 +490,7 @@ func TestHandler_deleteAPIKey_NoAuthService(t *testing.T) { func TestHandler_deleteAPIKey_NoToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{} @@ -482,6 +503,7 @@ func TestHandler_deleteAPIKey_NoToken(t *testing.T) { func TestHandler_deleteAPIKey_InvalidPath(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -508,6 +530,7 @@ func TestHandler_deleteAPIKey_InvalidPath(t *testing.T) { func TestHandler_deleteAPIKey_InvalidUUID(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -534,6 +557,7 @@ func TestHandler_deleteAPIKey_InvalidUUID(t *testing.T) { func TestHandler_deleteAPIKey_ServiceError(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -561,6 +585,7 @@ func TestHandler_deleteAPIKey_ServiceError(t *testing.T) { func TestHandler_revokeAPIKey_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -602,6 +627,7 @@ func TestHandler_revokeAPIKey_NoAuthService(t *testing.T) { func TestHandler_revokeAPIKey_NoToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{} @@ -614,6 +640,7 @@ func TestHandler_revokeAPIKey_NoToken(t *testing.T) { func TestHandler_revokeAPIKey_InvalidPath(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -640,6 +667,7 @@ func TestHandler_revokeAPIKey_InvalidPath(t *testing.T) { func TestHandler_revokeAPIKey_InvalidUUID(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -666,6 +694,7 @@ func TestHandler_revokeAPIKey_InvalidUUID(t *testing.T) { func TestHandler_revokeAPIKey_ServiceError(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "user-123", Email: "user@example.com"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -696,6 +725,7 @@ func TestHandler_revokeAPIKey_ServiceError(t *testing.T) { func TestHandler_listAPIKeys_PermissionDenied(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "viewer-1", Email: "viewer@example.com"} mockAuth.On("ValidateSession", ctx, "viewer-token").Return(session, nil) @@ -716,6 +746,7 @@ func TestHandler_listAPIKeys_PermissionDenied(t *testing.T) { func TestHandler_createAPIKey_PermissionDenied(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "viewer-1", Email: "viewer@example.com"} mockAuth.On("ValidateSession", ctx, "viewer-token").Return(session, nil) @@ -737,6 +768,7 @@ func TestHandler_createAPIKey_PermissionDenied(t *testing.T) { func TestHandler_deleteAPIKey_PermissionDenied(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "viewer-1", Email: "viewer@example.com"} mockAuth.On("ValidateSession", ctx, "viewer-token").Return(session, nil) @@ -762,6 +794,7 @@ func TestHandler_deleteAPIKey_PermissionDenied(t *testing.T) { func TestHandler_revokeAPIKey_PermissionDenied(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "viewer-1", Email: "viewer@example.com"} mockAuth.On("ValidateSession", ctx, "viewer-token").Return(session, nil) diff --git a/internal/api/handler_auth_login_mfa_limit_test.go b/internal/api/handler_auth_login_mfa_limit_test.go index b243c67fe..09e4a14bf 100644 --- a/internal/api/handler_auth_login_mfa_limit_test.go +++ b/internal/api/handler_auth_login_mfa_limit_test.go @@ -81,6 +81,7 @@ func TestHandler_login_NoMFACodeSkipsPerEmailBucket(t *testing.T) { func TestHandler_login_MFALegRateLimiterErrorFailsClosed(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) rl := new(MockRateLimiter) t.Cleanup(func() { rl.AssertExpectations(t) }) rl.On("AllowWithIP", mock.Anything, mock.Anything, "login").Return(true, nil).Once() @@ -205,7 +206,9 @@ func TestHandler_login_MFATokenFormValidation(t *testing.T) { } { t.Run(name, func(t *testing.T) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) rl := new(MockRateLimiter) + t.Cleanup(func() { rl.AssertExpectations(t) }) rl.On("AllowWithIP", mock.Anything, mock.Anything, "login").Return(true, nil).Once() h := &Handler{auth: mockAuth, rateLimiter: rl} _, err := h.login(context.Background(), mfaTokenReq("198.51.100.1", body)) diff --git a/internal/api/handler_auth_me_permissions_scope_test.go b/internal/api/handler_auth_me_permissions_scope_test.go index 4f96ba857..5349653b8 100644 --- a/internal/api/handler_auth_me_permissions_scope_test.go +++ b/internal/api/handler_auth_me_permissions_scope_test.go @@ -136,6 +136,7 @@ func TestGetCurrentUserPermissions_ScopeLookupError_ReportsRestricted(t *testing func TestGetCurrentUserPermissions_AdminAPIKey_ReportsUnrestricted(t *testing.T) { m := new(MockAuthService) + t.Cleanup(func() { m.AssertExpectations(t) }) h := &Handler{auth: m, apiKey: "admin-key-619"} got := routeMePermissions(t, h, &events.LambdaFunctionURLRequest{ @@ -188,6 +189,7 @@ func TestGetCurrentUserPermissions_UserAPIKey_FlagMatchesRefreshGate(t *testing. } { t.Run(name, func(t *testing.T) { m := new(MockAuthService) + t.Cleanup(func() { m.AssertExpectations(t) }) m.On("ValidateUserAPIKeyAPI", mock.Anything, meScopeRawKey). Return(&auth.UserAPIKey{ID: meScopeKeyID}, &auth.User{ID: meScopeKeyOwnerID}, nil).Maybe() m.On("GetAPIKeyEffectivePermissionsAPI", mock.Anything, meScopeKeyID, meScopeKeyOwnerID). diff --git a/internal/api/handler_auth_test.go b/internal/api/handler_auth_test.go index 5ef9ee952..3488fab32 100644 --- a/internal/api/handler_auth_test.go +++ b/internal/api/handler_auth_test.go @@ -17,6 +17,7 @@ import ( func TestHandler_login_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) loginResp := &LoginResponse{ Token: "test-token", @@ -65,6 +66,7 @@ func TestHandler_login_NoAuthService(t *testing.T) { func TestHandler_login_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, rateLimiter: NewInMemoryRateLimiter()} req := &events.LambdaFunctionURLRequest{ @@ -80,6 +82,7 @@ func TestHandler_login_InvalidBody(t *testing.T) { func TestHandler_login_AuthError(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("Login", ctx, mock.Anything).Return(nil, errors.New("invalid credentials")) @@ -142,6 +145,7 @@ func TestHandler_login_OpaqueError_HidesInternalMessage(t *testing.T) { func TestHandler_logout_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("Logout", ctx, "test-token").Return(nil) @@ -179,6 +183,7 @@ func TestHandler_logout_NoAuthService(t *testing.T) { func TestHandler_logout_NoToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ @@ -194,6 +199,7 @@ func TestHandler_logout_NoToken(t *testing.T) { func TestHandler_logout_Error(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("Logout", ctx, "test-token").Return(errors.New("session not found")) @@ -218,6 +224,7 @@ func TestHandler_logout_Error(t *testing.T) { func TestHandler_getCurrentUser_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{ UserID: "12345678-1234-1234-1234-123456789abc", @@ -269,6 +276,7 @@ func TestHandler_getCurrentUser_NoAuthService(t *testing.T) { func TestHandler_getCurrentUser_NoToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ @@ -284,6 +292,7 @@ func TestHandler_getCurrentUser_NoToken(t *testing.T) { func TestHandler_getCurrentUser_InvalidSession(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "invalid-token").Return(nil, errors.New("invalid session")) @@ -304,6 +313,7 @@ func TestHandler_getCurrentUser_InvalidSession(t *testing.T) { func TestHandler_getCurrentUser_UserNotFound(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{ UserID: "12345678-1234-1234-1234-123456789abc", @@ -330,6 +340,7 @@ func TestHandler_getCurrentUser_UserNotFound(t *testing.T) { func TestHandler_checkAdminExists_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("CheckAdminExists", ctx).Return(true, nil) @@ -352,6 +363,7 @@ func TestHandler_checkAdminExists_Success(t *testing.T) { func TestHandler_checkAdminExists_NoAdmin(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("CheckAdminExists", ctx).Return(false, nil) @@ -392,6 +404,7 @@ func TestHandler_checkAdminExists_NoAuthService(t *testing.T) { func TestHandler_checkAdminExists_Error(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("CheckAdminExists", ctx).Return(false, errors.New("database error")) @@ -414,6 +427,7 @@ func TestHandler_checkAdminExists_Error(t *testing.T) { func TestHandler_setupAdmin_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) loginResp := &LoginResponse{ Token: "admin-token", @@ -472,6 +486,7 @@ func TestHandler_setupAdmin_NoAuthService(t *testing.T) { func TestHandler_setupAdmin_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, rateLimiter: NewInMemoryRateLimiter()} req := &events.LambdaFunctionURLRequest{ @@ -492,6 +507,7 @@ func TestHandler_setupAdmin_InvalidBody(t *testing.T) { func TestHandler_setupAdmin_Error(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("SetupAdmin", ctx, mock.Anything).Return(nil, errors.New("admin already exists")) @@ -515,6 +531,7 @@ func TestHandler_setupAdmin_Error(t *testing.T) { func TestHandler_forgotPassword_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("RequestPasswordReset", ctx, "user@example.com").Return(nil) @@ -542,6 +559,7 @@ func TestHandler_forgotPassword_NoAuthService(t *testing.T) { func TestHandler_forgotPassword_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{Body: `{invalid json}`} @@ -555,6 +573,7 @@ func TestHandler_forgotPassword_ErrorStillReturnsSuccess(t *testing.T) { // Even if the email doesn't exist, we return success to prevent email enumeration ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("RequestPasswordReset", ctx, "nonexistent@example.com").Return(errors.New("user not found")) @@ -571,6 +590,7 @@ func TestHandler_forgotPassword_ErrorStillReturnsSuccess(t *testing.T) { func TestHandler_resetPassword_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ConfirmPasswordReset", ctx, PasswordResetConfirm{ Token: "reset-token", @@ -605,6 +625,7 @@ func TestHandler_resetPassword_NoAuthService(t *testing.T) { func TestHandler_resetPassword_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, rateLimiter: NewInMemoryRateLimiter()} req := &events.LambdaFunctionURLRequest{Body: `{invalid json}`} @@ -685,6 +706,7 @@ func TestHandler_resetPasswordStatus_Used(t *testing.T) { func TestHandler_resetPasswordStatus_MissingToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth, rateLimiter: NewInMemoryRateLimiter()} req := &events.LambdaFunctionURLRequest{QueryStringParameters: map[string]string{}} @@ -802,6 +824,7 @@ func TestHandler_resetPassword_ErrorIsClientError(t *testing.T) { func TestHandler_updateProfile_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{ UserID: "12345678-1234-1234-1234-123456789abc", @@ -851,6 +874,7 @@ func TestHandler_updateProfile_NoAuthService(t *testing.T) { func TestHandler_updateProfile_NoToken(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ @@ -867,6 +891,7 @@ func TestHandler_updateProfile_NoToken(t *testing.T) { func TestHandler_updateProfile_InvalidSession(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "invalid-token").Return(nil, errors.New("invalid session")) @@ -890,6 +915,7 @@ func TestHandler_updateProfile_InvalidSession(t *testing.T) { func TestHandler_changePassword_Success(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{ UserID: "11111111-1111-1111-1111-111111111111", @@ -939,6 +965,7 @@ func TestHandler_changePassword_NoAuthService(t *testing.T) { func TestHandler_changePassword_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{ UserID: "11111111-1111-1111-1111-111111111111", @@ -965,6 +992,7 @@ func TestHandler_changePassword_InvalidBody(t *testing.T) { func TestHandler_updateProfile_InvalidJSON(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "12345678-1234-1234-1234-123456789abc"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -989,6 +1017,7 @@ func TestHandler_updateProfile_InvalidJSON(t *testing.T) { func TestHandler_updateProfile_RejectsInvalidEmail(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "12345678-1234-1234-1234-123456789abc"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -1017,6 +1046,7 @@ func TestHandler_updateProfile_RejectsInvalidEmail(t *testing.T) { func TestHandler_updateProfile_AcceptsValidEmail(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) session := &Session{UserID: "12345678-1234-1234-1234-123456789abc"} mockAuth.On("ValidateSession", ctx, "test-token").Return(session, nil) @@ -1186,6 +1216,7 @@ func TestHandler_resetPassword_DecodesBase64(t *testing.T) { func TestHandler_resetPassword_InvalidBase64(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) handler := &Handler{auth: mockAuth} req := &events.LambdaFunctionURLRequest{ @@ -1205,6 +1236,7 @@ func TestHandler_resetPassword_InvalidBase64(t *testing.T) { func TestHandler_resetPassword_ClientErrorSubstrings(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ConfirmPasswordReset", ctx, mock.Anything). Return(errors.New("password must contain a number")) @@ -1228,6 +1260,7 @@ func TestHandler_resetPassword_ClientErrorSubstrings(t *testing.T) { func TestHandler_resetPassword_ServerSideErrorPassesThrough(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ConfirmPasswordReset", ctx, mock.Anything). Return(errors.New("database connection lost")) diff --git a/internal/api/handler_config_propagation_integration_test.go b/internal/api/handler_config_propagation_integration_test.go index ba9bfaf04..3904220d4 100644 --- a/internal/api/handler_config_propagation_integration_test.go +++ b/internal/api/handler_config_propagation_integration_test.go @@ -32,6 +32,7 @@ func TestUpdateConfigPropagatesOnlyChangedDefaults(t *testing.T) { require.NoError(t, store.SaveServiceConfig(ctx, &config.ServiceConfig{Provider: "aws", Service: "rds", Enabled: true, Term: 1, Payment: "partial-upfront", Coverage: 60, RampSchedule: config.RampMonthly10Pct})) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com"}, nil) mockAuth.grantAdmin() handler := &Handler{config: store, auth: mockAuth} @@ -124,6 +125,7 @@ func TestUpdateConfigPaymentDefaultSkipsAzureAndGCP(t *testing.T) { } mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com"}, nil) mockAuth.grantAdmin() handler := &Handler{config: store, auth: mockAuth} diff --git a/internal/api/handler_config_service_race_integration_test.go b/internal/api/handler_config_service_race_integration_test.go index a2b20aa5c..6a8db978f 100644 --- a/internal/api/handler_config_service_race_integration_test.go +++ b/internal/api/handler_config_service_race_integration_test.go @@ -41,6 +41,7 @@ func newServiceConfigRaceFixture(ctx context.Context, t *testing.T) *serviceConf pg := testhelpers.RequireMigratedDatabase(ctx, t, getMigrationsPath()) store := config.NewPostgresStore(pg.DB) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", }, nil) @@ -167,6 +168,7 @@ func TestUpdateServiceConfig_ParallelSettingsSaveDoesNotExhaustPool(t *testing.T store := config.NewPostgresStore(db) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", mock.Anything, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", }, nil) diff --git a/internal/api/handler_config_stamps_test.go b/internal/api/handler_config_stamps_test.go index 909b0c767..aff74bc86 100644 --- a/internal/api/handler_config_stamps_test.go +++ b/internal/api/handler_config_stamps_test.go @@ -23,6 +23,7 @@ func TestGetConfig_ServiceRowsCarryStamps(t *testing.T) { updated := created.Add(26 * time.Hour) mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetGlobalConfig", mock.Anything).Return(&config.GlobalConfig{EnabledProviders: []string{"aws"}}, nil) mockStore.On("ListServiceConfigs", mock.Anything).Return([]config.ServiceConfig{ {Provider: "aws", Service: "ec2", CreatedAt: &created, UpdatedAt: &updated}, @@ -57,12 +58,14 @@ func TestGetConfig_ServiceRowsCarryStamps(t *testing.T) { func TestUpdateServiceConfig_IgnoresClientStamps(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", }, nil) mockAuth.grantAdmin() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockStore.On("GetServiceConfig", ctx, "aws", "rds").Return(nil, nil) var saved *config.ServiceConfig mockStore.On("SaveServiceConfig", ctx, mock.AnythingOfType("*config.ServiceConfig")). diff --git a/internal/api/handler_config_test.go b/internal/api/handler_config_test.go index 8c02c371a..184449301 100644 --- a/internal/api/handler_config_test.go +++ b/internal/api/handler_config_test.go @@ -18,7 +18,9 @@ import ( func TestHandler_getConfig(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -55,7 +57,9 @@ func TestHandler_getConfig(t *testing.T) { func TestHandler_updateConfig(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -453,6 +457,7 @@ func (s *serializedConfigStore) ListServiceConfigs(_ context.Context) ([]config. func TestHandler_updateConfig_ConcurrentPartialPUTsNoLostUpdate(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token"). Return(&Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com"}, nil) mockAuth.grantAdmin() @@ -510,6 +515,7 @@ func TestHandler_updateConfig_ConcurrentPartialPUTsNoLostUpdate(t *testing.T) { func TestHandler_updateConfig_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -537,7 +543,9 @@ func TestHandler_updateConfig_InvalidBody(t *testing.T) { func TestHandler_getServiceConfig(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -572,6 +580,7 @@ func TestHandler_getServiceConfig(t *testing.T) { func TestHandler_getServiceConfig_InvalidFormat(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) mockAuth.grantAdmin() @@ -590,7 +599,9 @@ func TestHandler_getServiceConfig_InvalidFormat(t *testing.T) { func TestHandler_getServiceConfig_NotFound(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) @@ -613,7 +624,9 @@ func TestHandler_getServiceConfig_NotFound(t *testing.T) { func TestHandler_updateServiceConfig(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -764,11 +777,13 @@ func TestHandler_updateServiceConfig_PartialPut(t *testing.T) { ctx := context.Background() newHandler := func(existing *config.ServiceConfig) (*Handler, *MockConfigStore) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", }, nil) mockAuth.grantAdmin() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetServiceConfig", ctx, existing.Provider, existing.Service).Return(existing, nil) return &Handler{config: store, auth: mockAuth}, store } @@ -817,11 +832,13 @@ func TestHandler_updateServiceConfig_PartialPut(t *testing.T) { func TestHandler_updateServiceConfig_MergesInsideAtomicUpdate(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", }, nil) mockAuth.grantAdmin() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) stored := &config.ServiceConfig{ Provider: "aws", Service: "rds", Enabled: true, Term: 3, Payment: "all-upfront", Coverage: 55, MinCount: 4, @@ -861,6 +878,7 @@ func TestHandler_updateServiceConfig_CommitmentOptsOutsideTransaction(t *testing run := func(t *testing.T, stored *config.ServiceConfig, body string, getErr error) error { t.Helper() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", }, nil) @@ -870,9 +888,12 @@ func TestHandler_updateServiceConfig_CommitmentOptsOutsideTransaction(t *testing // read is the point where the real store has opened its transaction. txOpened, gets := false, 0 store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetServiceConfig", ctx, "aws", "rds").Return(stored, nil). Run(func(mock.Arguments) { txOpened = true }) - store.On("SaveServiceConfig", ctx, mock.AnythingOfType("*config.ServiceConfig")).Return(nil) + // A refused PUT saves nothing; which cases save is checked after the + // call. + store.On("SaveServiceConfig", ctx, mock.AnythingOfType("*config.ServiceConfig")).Return(nil).Maybe() h := &Handler{config: store, auth: mockAuth, commitmentOpts: &stubCommitmentOpts{ getFn: func(context.Context) (commitmentopts.Options, error) { assert.False(t, txOpened, "commitment options read inside the transaction") @@ -888,6 +909,11 @@ func TestHandler_updateServiceConfig_CommitmentOptsOutsideTransaction(t *testing Headers: map[string]string{"Authorization": "Bearer admin-token"}, Body: body, }, "aws/rds") assert.Equal(t, 1, gets, "options fetched exactly once, before the transaction") + if err == nil { + store.AssertNumberOfCalls(t, "SaveServiceConfig", 1) + } else { + store.AssertNotCalled(t, "SaveServiceConfig", mock.Anything, mock.Anything) + } return err } storedThreeYear := &config.ServiceConfig{Provider: "aws", Service: "rds", Enabled: true, Term: 3, Payment: "no-upfront", Coverage: 50} @@ -922,11 +948,13 @@ func TestHandler_updateServiceConfig_StoredRowRead(t *testing.T) { ctx := context.Background() newHandler := func(readErr error) (*Handler, *MockConfigStore) { mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", }, nil) mockAuth.grantAdmin() store := new(MockConfigStore) + t.Cleanup(func() { store.AssertExpectations(t) }) store.On("GetServiceConfig", ctx, "aws", "rds").Return(nil, readErr) return &Handler{config: store, auth: mockAuth}, store } @@ -960,6 +988,7 @@ func TestHandler_updateServiceConfig_StoredRowRead(t *testing.T) { func TestHandler_updateServiceConfig_InvalidBody(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -987,7 +1016,9 @@ func TestHandler_updateServiceConfig_InvalidBody(t *testing.T) { func TestHandler_updateServiceConfig_CommitmentOptsReject(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -1034,7 +1065,9 @@ func TestHandler_updateServiceConfig_CommitmentOptsReject(t *testing.T) { func TestHandler_updateServiceConfig_CommitmentOptsAccept(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -1073,7 +1106,9 @@ func TestHandler_updateServiceConfig_CommitmentOptsAccept(t *testing.T) { func TestHandler_updateServiceConfig_CommitmentOptsErrorRefusesSave(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) mockAuth.On("ValidateSession", ctx, "admin-token").Return(&Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Email: "admin@example.com", @@ -1108,7 +1143,9 @@ func TestHandler_updateServiceConfig_CommitmentOptsErrorRefusesSave(t *testing.T func TestHandler_updateServiceConfig_NoSlash(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -1137,7 +1174,9 @@ func TestHandler_updateServiceConfig_NoSlash(t *testing.T) { func TestHandler_getConfig_GlobalConfigError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) @@ -1157,7 +1196,9 @@ func TestHandler_getConfig_GlobalConfigError(t *testing.T) { func TestHandler_getConfig_ListServiceConfigsError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) @@ -1186,7 +1227,9 @@ func TestHandler_getConfig_ListServiceConfigsError(t *testing.T) { func TestHandler_getConfig_SourceIdentity_AdminOnly(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "admin-user"} userSession := &Session{UserID: "regular-user"} @@ -1232,7 +1275,9 @@ func TestHandler_getConfig_SourceIdentity_AdminOnly(t *testing.T) { func TestHandler_getServiceConfig_Error(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) @@ -1252,7 +1297,9 @@ func TestHandler_getServiceConfig_Error(t *testing.T) { func TestHandler_updateConfig_ValidationError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -1315,7 +1362,9 @@ func TestUpdateConfig_RejectsOutOfRangeRIExchangeLimits(t *testing.T) { func TestHandler_updateConfig_SaveError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -1349,7 +1398,9 @@ func TestHandler_updateConfig_SaveError(t *testing.T) { func TestHandler_updateServiceConfig_SaveError(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{ UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", @@ -1378,6 +1429,7 @@ func TestHandler_updateServiceConfig_SaveError(t *testing.T) { func TestHandler_getServiceConfig_InvalidProvider(t *testing.T) { ctx := context.Background() mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) adminSession := &Session{UserID: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"} mockAuth.On("ValidateSession", ctx, "admin-token").Return(adminSession, nil) mockAuth.grantAdmin() @@ -1461,7 +1513,9 @@ func TestHandler_updateConfig_PropagationErrorIsReturned(t *testing.T) { func TestHandler_getConfig_ViewConfigPermission_Enforced(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) userSession := &Session{ UserID: "cccccccc-cccc-cccc-cccc-cccccccccccc", @@ -1488,7 +1542,9 @@ func TestHandler_getConfig_ViewConfigPermission_Enforced(t *testing.T) { func TestHandler_getServiceConfig_ViewConfigPermission_Enforced(t *testing.T) { ctx := context.Background() mockStore := new(MockConfigStore) + t.Cleanup(func() { mockStore.AssertExpectations(t) }) mockAuth := new(MockAuthService) + t.Cleanup(func() { mockAuth.AssertExpectations(t) }) userSession := &Session{ UserID: "cccccccc-cccc-cccc-cccc-cccccccccccc",