155 lines
5.6 KiB
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")
|
|
}
|
|
}
|
|
}
|