fix(auth): harden restore verification transaction
This commit is contained in:
@@ -466,8 +466,109 @@ func TestRestoreRefusesActiveSessionsWithoutDrain(t *testing.T) {
|
||||
if !errors.Is(err, ErrActiveSessions) {
|
||||
t.Fatalf("restore error = %v, want active-session refusal", err)
|
||||
}
|
||||
if restoredFiles != 0 || runner.stopCount != 0 || runner.startCount != 0 || !runner.running {
|
||||
t.Fatalf("active-session refusal mutated target: files=%d stops=%d starts=%d running=%t", restoredFiles, runner.stopCount, runner.startCount, runner.running)
|
||||
if restoredFiles != 0 || runner.stopCount != 0 || runner.startCount != 0 || !runner.running || runner.maintenance {
|
||||
t.Fatalf("active-session refusal changed lifecycle state: files=%d stops=%d starts=%d running=%t maintenance=%t", restoredFiles, runner.stopCount, runner.startCount, runner.running, runner.maintenance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreCleansMaintenanceAfterActivationFailure(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
backing := newBackupRunner(installation, true)
|
||||
activationErr := errors.New("activation response lost")
|
||||
runner := &restoreFailureRunner{
|
||||
fakeBackupRunner: backing,
|
||||
failContains: "operator-command.js maintenance-activate",
|
||||
err: activationErr,
|
||||
beforeFailure: func() { backing.maintenance = true },
|
||||
}
|
||||
deps := restoreTestDependencies(t, runner)
|
||||
|
||||
_, err := restoreWithDependencies(context.Background(), installation, RestoreRequest{Archive: restoreArchive(t), Confirm: true}, deps)
|
||||
if !errors.Is(err, activationErr) {
|
||||
t.Fatalf("restore error = %v, want activation failure", err)
|
||||
}
|
||||
if backing.maintenance || !backing.running || runner.matchCount != 1 {
|
||||
t.Fatalf("activation cleanup state: maintenance=%t running=%t matches=%d", backing.maintenance, backing.running, runner.matchCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreRestartsAndCleansMaintenanceAfterStopFailure(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
backing := newBackupRunner(installation, true)
|
||||
stopErr := errors.New("stop response lost")
|
||||
runner := &restoreFailureRunner{
|
||||
fakeBackupRunner: backing,
|
||||
failSuffix: " stop",
|
||||
err: stopErr,
|
||||
beforeFailure: func() {
|
||||
backing.stopCount++
|
||||
backing.running, backing.coreRunning = false, false
|
||||
},
|
||||
}
|
||||
deps := restoreTestDependencies(t, runner)
|
||||
|
||||
_, err := restoreWithDependencies(context.Background(), installation, RestoreRequest{Archive: restoreArchive(t), Confirm: true}, deps)
|
||||
if !errors.Is(err, stopErr) {
|
||||
t.Fatalf("restore error = %v, want stop failure", err)
|
||||
}
|
||||
if backing.maintenance || !backing.running || backing.startCount != 1 {
|
||||
t.Fatalf("stop cleanup state: maintenance=%t running=%t starts=%d", backing.maintenance, backing.running, backing.startCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreCleansMaintenanceAfterMutationAndRollbackFailures(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
recoveryErr error
|
||||
}{
|
||||
{name: "mutation"},
|
||||
{name: "rollback", recoveryErr: errors.New("rollback failed")},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
backing := newBackupRunner(installation, true)
|
||||
deps := restoreTestDependencies(t, backing)
|
||||
mutationErr := errors.New("mutation failed")
|
||||
deps.restoreFile = func(context.Context, config.Installation, ArchiveEntryMetadata, io.Reader) error {
|
||||
return mutationErr
|
||||
}
|
||||
deps.recover = func(context.Context, config.Installation, PreflightResult, bool) error {
|
||||
if test.recoveryErr == nil {
|
||||
backing.running, backing.coreRunning = true, true
|
||||
}
|
||||
return test.recoveryErr
|
||||
}
|
||||
|
||||
_, err := restoreWithDependencies(context.Background(), installation, RestoreRequest{Archive: restoreArchive(t), Confirm: true}, deps)
|
||||
if !errors.Is(err, mutationErr) || (test.recoveryErr != nil && !errors.Is(err, test.recoveryErr)) {
|
||||
t.Fatalf("restore error = %v, want mutation and rollback failures", err)
|
||||
}
|
||||
if backing.maintenance || !backing.running {
|
||||
t.Fatalf("failure cleanup state: maintenance=%t running=%t", backing.maintenance, backing.running)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestorePreservesDrainAndMaintenanceCleanupFailures(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
backing := newBackupRunner(installation, true)
|
||||
backing.sessionResponses = []string{`[{"status":"running","archived":false}]`}
|
||||
cleanupErr := errors.New("maintenance cleanup failed")
|
||||
runner := &restoreFailureRunner{
|
||||
fakeBackupRunner: backing,
|
||||
failContains: "operator-command.js maintenance-deactivate",
|
||||
err: cleanupErr,
|
||||
beforeFailure: func() { backing.maintenance = false },
|
||||
}
|
||||
deps := restoreTestDependencies(t, runner)
|
||||
|
||||
_, err := restoreWithDependencies(context.Background(), installation, RestoreRequest{Archive: restoreArchive(t), Confirm: true}, deps)
|
||||
if !errors.Is(err, ErrActiveSessions) || !errors.Is(err, cleanupErr) {
|
||||
t.Fatalf("restore error = %v, want drain and cleanup failures", err)
|
||||
}
|
||||
if backing.maintenance || !backing.running {
|
||||
t.Fatalf("cleanup failure state: maintenance=%t running=%t", backing.maintenance, backing.running)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +592,37 @@ func (runner failStartRestoreRunner) Run(ctx context.Context, args []string, std
|
||||
return runner.fakeBackupRunner.Run(ctx, args, stdin)
|
||||
}
|
||||
|
||||
type restoreFailureRunner struct {
|
||||
*fakeBackupRunner
|
||||
failContains string
|
||||
failSuffix string
|
||||
err error
|
||||
beforeFailure func()
|
||||
matchCount int
|
||||
}
|
||||
|
||||
func (runner *restoreFailureRunner) Run(ctx context.Context, args []string, stdin io.Reader) (compose.Result, error) {
|
||||
command := strings.Join(args, " ")
|
||||
matches := runner.failContains != "" && strings.Contains(command, runner.failContains)
|
||||
matches = matches || runner.failSuffix != "" && strings.HasSuffix(command, runner.failSuffix)
|
||||
if matches {
|
||||
runner.matchCount++
|
||||
if runner.beforeFailure != nil {
|
||||
runner.beforeFailure()
|
||||
}
|
||||
return compose.Result{}, runner.err
|
||||
}
|
||||
return runner.fakeBackupRunner.Run(ctx, args, stdin)
|
||||
}
|
||||
|
||||
func (runner *restoreFailureRunner) Stream(ctx context.Context, args []string, stdin io.Reader, stdout io.Writer) (compose.Result, error) {
|
||||
return runner.fakeBackupRunner.Stream(ctx, args, stdin, stdout)
|
||||
}
|
||||
|
||||
func (runner *restoreFailureRunner) SessionInventoryScope() string {
|
||||
return runner.fakeBackupRunner.SessionInventoryScope()
|
||||
}
|
||||
|
||||
type fakeRestoreLock struct {
|
||||
release func()
|
||||
}
|
||||
@@ -542,14 +674,15 @@ func (*workspaceVerificationRunner) SessionInventoryScope() string { return "min
|
||||
|
||||
func TestVerifyRestoreWorkspaceUsesFixedNonNetworkOperatorPath(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
fingerprint := "sha256:" + strings.Repeat("a", 64)
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
running bool
|
||||
payload string
|
||||
prefix string
|
||||
}{
|
||||
{name: "stopped uninitialized", payload: `{"ready":true,"state":"uninitialized","workspaces":0}`, prefix: "run --rm --no-deps --no-TTY core"},
|
||||
{name: "running active", running: true, payload: `{"ready":true,"state":"active","workspaces":1}`, prefix: "exec -T core"},
|
||||
{name: "stopped uninitialized", payload: fmt.Sprintf(`{"ready":true,"state":"uninitialized","workspaces":0,"fingerprint":%q}`, fingerprint), prefix: "run --rm --no-deps --no-TTY core"},
|
||||
{name: "running active", running: true, payload: fmt.Sprintf(`{"ready":true,"state":"active","workspaces":1,"fingerprint":%q}`, fingerprint), prefix: "exec -T core"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
runner := &workspaceVerificationRunner{
|
||||
@@ -573,6 +706,7 @@ func TestVerifyRestoreWorkspaceUsesFixedNonNetworkOperatorPath(t *testing.T) {
|
||||
|
||||
func TestVerifyRestoreWorkspaceRejectsInvalidOperatorResults(t *testing.T) {
|
||||
installation := preflightTestInstallation(t)
|
||||
valid := `{"ready":true,"state":"active","workspaces":1,"fingerprint":"sha256:` + strings.Repeat("a", 64) + `"}`
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
result compose.Result
|
||||
@@ -580,11 +714,13 @@ func TestVerifyRestoreWorkspaceRejectsInvalidOperatorResults(t *testing.T) {
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "malformed", result: compose.Result{Stdout: `{malformed`}},
|
||||
{name: "trailing document", result: compose.Result{Stdout: `{"ready":true,"state":"active","workspaces":1}{}`}},
|
||||
{name: "unknown field", result: compose.Result{Stdout: `{"ready":true,"state":"active","workspaces":1,"detail":"unsafe"}`}},
|
||||
{name: "not ready", result: compose.Result{Stdout: `{"ready":false,"state":"uninitialized","workspaces":0}`}},
|
||||
{name: "unknown state", result: compose.Result{Stdout: `{"ready":true,"state":"unknown","workspaces":0}`}},
|
||||
{name: "inconsistent count", result: compose.Result{Stdout: `{"ready":true,"state":"uninitialized","workspaces":1}`}},
|
||||
{name: "trailing document", result: compose.Result{Stdout: valid + `{}`}},
|
||||
{name: "unknown field", result: compose.Result{Stdout: strings.TrimSuffix(valid, "}") + `,"detail":"unsafe"}`}},
|
||||
{name: "not ready", result: compose.Result{Stdout: strings.Replace(valid, `"ready":true`, `"ready":false`, 1)}},
|
||||
{name: "unknown state", result: compose.Result{Stdout: strings.Replace(valid, `"state":"active"`, `"state":"unknown"`, 1)}},
|
||||
{name: "inconsistent count", result: compose.Result{Stdout: strings.Replace(valid, `"state":"active"`, `"state":"uninitialized"`, 1)}},
|
||||
{name: "missing fingerprint", result: compose.Result{Stdout: `{"ready":true,"state":"active","workspaces":1}`}},
|
||||
{name: "malformed fingerprint", result: compose.Result{Stdout: `{"ready":true,"state":"active","workspaces":1,"fingerprint":"sha256:not-a-digest"}`}},
|
||||
{name: "nonzero", result: compose.Result{ExitCode: 2}, err: errors.New("exit status 2")},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user