Scert/backend/internal/adminconsole/recovery_disposition_test.go

155 lines
5.6 KiB
Go

package adminconsole
import (
"context"
"errors"
"strings"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
)
type dispositionConsoleClient struct {
recoveryTestClient
prepares int
executes int
queries int
action deployment.RecoveryAction
executeError bool
expired bool
status string
}
func (c *dispositionConsoleClient) PrepareRecovery(id string, action deployment.RecoveryAction) (localreset.RecoveryPreparation, error) {
c.prepares++
c.action = action
expires := time.Now().Add(time.Minute)
if c.expired {
expires = time.Now().Add(-time.Second)
}
return localreset.RecoveryPreparation{DispositionID: strings.Repeat("b", 64), Token: strings.Repeat("c", 64), Summary: deployment.RecoveryReviewSummary{
TransactionID: id, ServerID: "server-1", Phase: "reload_attempted", Action: action, HistoricalEvidence: true, ObservedAt: time.Now(), ExpiresAt: expires,
}}, nil
}
func (c *dispositionConsoleClient) ExecuteRecovery(id, token string) (deployment.RecoveryDispositionSummary, error) {
c.executes++
if c.executeError {
return deployment.RecoveryDispositionSummary{}, errors.New("connection-lost")
}
return c.result(id), nil
}
func (c *dispositionConsoleClient) GetRecoveryDisposition(id string) (deployment.RecoveryDispositionSummary, error) {
c.queries++
return c.result(id), nil
}
func (c *dispositionConsoleClient) result(id string) deployment.RecoveryDispositionSummary {
now := time.Now().UTC()
status := c.status
if status == "" {
status = "succeeded"
}
codes := map[deployment.RecoveryAction]string{deployment.RecoveryActionRecord: "observed", deployment.RecoveryActionCommit: "committed", deployment.RecoveryActionRollback: "rolled_back"}
code := codes[c.action]
if status == "running" {
code = ""
} else if status != "succeeded" {
code = status
}
return deployment.RecoveryDispositionSummary{ID: id, TransactionID: "tx-1", ServerID: "server-1", Action: c.action, Status: status, ResultCode: code, CreatedAt: now, UpdatedAt: now}
}
func TestDispositionConsoleExplicitActions(t *testing.T) {
for _, row := range []struct {
choice string
action deployment.RecoveryAction
message string
}{
{"1", deployment.RecoveryActionRecord, "核查结论已记录"},
{"2", deployment.RecoveryActionCommit, "本次人工提交核验"},
{"3", deployment.RecoveryActionRollback, "本次恢复基线核验"},
} {
t.Run(row.choice, func(t *testing.T) {
client := &dispositionConsoleClient{}
screen := &recoveryTestScreen{testTerminalInput: testTerminalInput{values: []string{"tx-1", row.choice, "确认 tx-1 " + dispositionActionLabel(row.action)}}}
if err := recoveryDispositionFlow(t.Context(), client, screen); err != nil {
t.Fatal(err)
}
output := strings.Join(screen.lines, "|")
if client.action != row.action || client.prepares != 1 || client.executes != 1 || client.queries != 0 || !strings.Contains(output, row.message) {
t.Fatal("wrong action or result")
}
if strings.Contains(output, strings.Repeat("c", 64)) || !strings.Contains(output, strings.Repeat("b", 64)) || !strings.Contains(output, "禁止重复执行") {
t.Fatal("token leaked or recovery guidance absent")
}
})
}
}
func TestDispositionConsoleCancellationAndUncertainResult(t *testing.T) {
for _, mode := range []string{"quit", "cancel-action", "wrong-confirmation", "expired", "cancelled", "display-error", "disconnect"} {
t.Run(mode, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
client := &dispositionConsoleClient{}
screen := &recoveryTestScreen{testTerminalInput: testTerminalInput{values: []string{"tx-1", "2", "确认 tx-1 完成提交"}}}
switch mode {
case "quit":
screen.values[0] = "q"
case "cancel-action":
screen.values[1] = "q"
case "wrong-confirmation":
screen.values[2] = "确认 tx-1 恢复基线"
case "expired":
client.expired = true
case "cancelled":
screen.readHook = func(index int) {
if index == 3 {
cancel()
}
}
case "display-error":
screen.showError = true
case "disconnect":
client.executeError = true
}
_ = recoveryDispositionFlow(ctx, client, screen)
expected := 0
if mode == "disconnect" {
expected = 1
}
if client.executes != expected || client.queries != 0 {
t.Fatalf("unexpected retry or cancelled execution: %d", client.executes)
}
output := strings.Join(screen.lines, "|")
if strings.Contains(output, "本次人工提交核验及结束审计完成") {
t.Fatal("failure reported success")
}
if mode == "disconnect" && !strings.Contains(output, "不能断言未执行或执行成功") {
t.Fatal("missing uncertainty warning")
}
})
}
}
func TestDispositionConsoleQueryOnlyAndStatusWarnings(t *testing.T) {
for _, status := range []string{"running", "uncertain", "rejected", "succeeded"} {
client := &dispositionConsoleClient{status: status, action: deployment.RecoveryActionRecord}
screen := &recoveryTestScreen{testTerminalInput: testTerminalInput{values: []string{strings.Repeat("b", 64)}}}
if err := recoveryResultFlow(t.Context(), client, screen); err != nil {
t.Fatal(err)
}
if client.prepares != 0 || client.executes != 0 || client.queries != 1 {
t.Fatal("query changed state")
}
output := strings.Join(screen.lines, "|")
if (status == "running" || status == "uncertain") && !strings.Contains(output, "禁止重复执行") {
t.Fatal("missing warning")
}
if status == "succeeded" && !strings.Contains(output, "未改变原事务终态") {
t.Fatal("record action falsely claims recovery")
}
}
}