Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/api/account_scope_fail_closed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) })

Expand Down Expand Up @@ -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).
Expand All @@ -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).
Expand Down
8 changes: 8 additions & 0 deletions internal/api/account_scope_shared_name_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}})
Expand Down
1 change: 1 addition & 0 deletions internal/api/analytics_account_ids_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion internal/api/apikey_constraints_roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))

Expand Down
5 changes: 4 additions & 1 deletion internal/api/apikey_explicit_zero_cap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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+`}`))
Expand Down
5 changes: 5 additions & 0 deletions internal/api/authpublic_csrf_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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",
Expand All @@ -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) {
Expand Down
33 changes: 22 additions & 11 deletions internal/api/auto_purchase_grant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions internal/api/claimed_row_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -89,13 +91,15 @@ 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)
}

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)
Expand All @@ -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)})
Expand All @@ -127,13 +133,15 @@ 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)
}

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)
Expand Down Expand Up @@ -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{
Expand All @@ -221,13 +231,15 @@ 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)
}

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)
Expand Down
2 changes: 2 additions & 0 deletions internal/api/combined_plan_purchase_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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})
Expand Down
Loading
Loading