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
5 changes: 4 additions & 1 deletion internal/purchase/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func (m *Manager) executeForAccountLate(ctx context.Context, baseExec *config.Pu
if err != nil {
acctExec.Status = "failed"
acctExec.Error = err.Error()
credErr := fmt.Errorf("credential resolution failed for account %s: %w", account.ID, err)
credErr := beforeSubmission(fmt.Errorf("credential resolution failed for account %s: %w", account.ID, err))
// Audit-trail save: the failed row is as much a part of the audit
// trail as a success row, so a save failure must surface (AUDIT LOSS)
// instead of being silently dropped. Extracted to a helper to keep
Expand Down Expand Up @@ -499,6 +499,9 @@ func skippedAccountRows(scoped map[string]*config.PurchaseExecution, results []e
// #1184 / COR-10). Extracted from executeForAccount so the new branch keeps
// that function under the gocyclo:10 budget.
func (m *Manager) persistFailedAccountExecution(ctx context.Context, acctExec *config.PurchaseExecution, account config.CloudAccount, cause error) error {
// Stamped before the save so the row that lands carries the proof; a failed
// save loses the row and strips the class below.
markRefusedAccountRow(acctExec, refusedBeforeSubmission(cause))
if saveErr := m.saveAccountRow(ctx, acctExec); saveErr != nil {
// The cause is joined as text: a lost audit row means the outcome is
// unknown, so a before-submission class on the cause must not survive
Expand Down
59 changes: 59 additions & 0 deletions internal/purchase/fanout_cred_pin_rows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package purchase

// #1844 S2: a credential or pin failure happens before any purchase call, so
// its account row records no_submission (releasing it from the ramp) and counts
// as proof for the root, unless the row was lost or another account may have
// submitted.

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestExecuteMultiAccount_CredentialAndPinRows(t *testing.T) {
bothRefused := map[string]recBehavior{skipAcctA: recRefused, skipAcctB: recRefused}
cases := map[string]struct {
fx fanOutFixture
wantProof bool
wantRowA bool // account A's row exists
flaggedA bool
}{
"a credential failure row is flagged and the root proven": {
fx: fanOutFixture{behaviors: bothRefused, credFail: map[string]bool{skipAcctA: true, skipAcctB: true}},
wantProof: true, wantRowA: true, flaggedA: true,
},
"a pin failure row is flagged and the root proven": {
fx: fanOutFixture{behaviors: bothRefused, pinFail: true},
wantProof: true, wantRowA: true, flaggedA: true,
},
"a credential failure next to a started plain failure stays unproven": {
fx: fanOutFixture{
behaviors: map[string]recBehavior{skipAcctA: recRefused, skipAcctB: recPlainFail},
credFail: map[string]bool{skipAcctA: true},
},
wantRowA: true, flaggedA: true,
},
"a lost credential row stays unproven and unflagged": {
fx: fanOutFixture{behaviors: bothRefused, credFail: map[string]bool{skipAcctA: true}, saveFail: map[string]bool{skipAcctA: true}},
wantProof: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := tc.fx.runRoot(t)
require.ErrorIs(t, r.err, errAllAccountsFailed)
row := savedAccountRow(r.saved, skipAcctA)
if !tc.wantRowA {
assert.Nil(t, row)
} else {
require.NotNil(t, row)
assert.Equal(t, "failed", row.Status)
assert.Equal(t, tc.flaggedA, row.NoSubmission)
}
r.saveTerminal(t)
assert.Equal(t, tc.wantProof, r.root.NoSubmission, "root no_submission")
})
}
}
3 changes: 2 additions & 1 deletion internal/purchase/fanout_refusal_fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ func TestExecuteMultiAccount_FanOutFixtureHooks(t *testing.T) {
"credential failure on A, B refused": {
fx: fanOutFixture{behaviors: refusedBoth, credFail: map[string]bool{skipAcctA: true}},
errContain: "credential resolution failed for account " + skipAcctA,
a: rowWant{"failed", false}, // the unflagged row #1844 S2 will flag
wantProof: true,
a: rowWant{"failed", true},
b: rowWant{"failed", true},
},
"row save failure on A (AUDIT LOSS), B refused": {
Expand Down
4 changes: 2 additions & 2 deletions internal/purchase/fanout_root_proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestExecuteMultiAccount_RootProof(t *testing.T) {
"3 one unknown outcome, one refused": {fx: fanOutFixture{behaviors: withA(recUnknown)}},
"4 refused, the other never started": {fx: fanOutFixture{behaviors: bothRefused, cancelAfterSave: skipAcctA}, wantProof: true},
"5 all never started": {fx: fanOutFixture{behaviors: bothRefused, precancel: true}, wantProof: true},
"5b started account cut off by the context": {fx: fanOutFixture{behaviors: bothRefused, cancelAtCred: skipAcctA}},
"5b started account cut off by the context": {fx: fanOutFixture{behaviors: bothRefused, cancelAtCred: skipAcctA}, wantProof: true}, // cut off in credential load: nothing submitted (#1844 S2)
"7 one panics after submitting, one refused": {fx: fanOutFixture{behaviors: withA(recPanics)}},
"8 refused wrapping unknown, one refused": {fx: fanOutFixture{behaviors: withA(recRefusedUnknown)}},
"9 audit loss on a refused account's row": {fx: fanOutFixture{behaviors: bothRefused, saveFail: map[string]bool{skipAcctA: true}}},
Expand All @@ -45,7 +45,7 @@ func TestExecuteMultiAccount_RootProof(t *testing.T) {
"9c provider factory failure, one refused": {fx: fanOutFixture{behaviors: bothRefused, factoryFail: map[string]bool{skipAcctA: true}}},
"9c service client failure, one refused": {fx: fanOutFixture{behaviors: bothRefused, clientFail: map[string]bool{skipAcctA: true}}},
"9c success=false result, one refused": {fx: fanOutFixture{behaviors: withA(recSuccessFalse)}},
"credential failure, one refused (until S2)": {fx: fanOutFixture{behaviors: bothRefused, credFail: map[string]bool{skipAcctA: true}}},
"credential failure, one refused": {fx: fanOutFixture{behaviors: bothRefused, credFail: map[string]bool{skipAcctA: true}}, wantProof: true},
"plain purchase failure, one refused": {fx: fanOutFixture{behaviors: withA(recPlainFail)}},
"never started, the started one failed plain": {fx: fanOutFixture{behaviors: withA(recPlainFail), cancelAfterSave: skipAcctA}},
}
Expand Down
Loading