122 lines
5.1 KiB
Go
122 lines
5.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
|
)
|
|
|
|
type localReconciliationJournal struct {
|
|
localDispositionJournal
|
|
source deployment.RecoveryDisposition
|
|
}
|
|
|
|
func (j *localReconciliationJournal) GetRecoveryDisposition(ctx context.Context, id string) (deployment.RecoveryDisposition, error) {
|
|
if id == j.source.Intent.ID {
|
|
return j.source, nil
|
|
}
|
|
return j.localDispositionJournal.GetRecoveryDisposition(ctx, id)
|
|
}
|
|
|
|
func localReconciliationFixture(t *testing.T) (*Server, *localResetSession, *localReconciliationJournal) {
|
|
t.Helper()
|
|
server, _, original := localDispositionFixture(t)
|
|
target := server.servers.(localDispositionServerRepo).server
|
|
journal := &localReconciliationJournal{}
|
|
journal.record = original.record
|
|
journal.record.Phase = deployment.TransactionPhaseCommitVerified
|
|
journal.record.Evidence = &deployment.RecoveryEvidence{Version: 1, LockToken: strings.Repeat("d", 64), Address: target.Address,
|
|
Port: target.Port, HostKeyFingerprint: target.HostKeyFingerprint, ValidationCommand: target.ValidationCommand, ReloadCommand: target.ReloadCommand,
|
|
Files: map[string]string{}, TemporaryFiles: map[string]string{}, Baseline: deployment.RecoveryBaseline{Endpoint: "https://example.test", Domains: []string{"example.test"}, ExpectedFingerprint: "new"}}
|
|
for _, path := range journal.record.Paths {
|
|
journal.record.LockPaths = append(journal.record.LockPaths, path+".scert-lock")
|
|
journal.record.Backups = append(journal.record.Backups, deployment.TransactionBackup{Original: path, Saved: path + ".scert-backup-" + journal.record.ID})
|
|
journal.record.Evidence.Files[path] = strings.Repeat("e", 64)
|
|
}
|
|
service := deployment.NewServiceWithJournal(localDispositionFactory{}, journal)
|
|
service.VerifyRecoveryTarget = func(context.Context, deployment.Server, deployment.RecoveryBaseline, bool) error { return nil }
|
|
resolve := func(context.Context, string) (deployment.Server, error) { return target, nil }
|
|
review, err := service.PrepareRecoveryReview(t.Context(), journal.record.ID, "uid:0", deployment.RecoveryActionCommit, resolve)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
intent, err := review.DispositionIntent(strings.Repeat("a", 64), strings.Repeat("b", 64))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now().UTC()
|
|
journal.source = deployment.RecoveryDisposition{Intent: intent, Status: "uncertain", ResultCode: "uncertain", CreatedAt: now, UpdatedAt: now}
|
|
server.recoveryInspection = service
|
|
return server, localDispositionNewSession(t, server), journal
|
|
}
|
|
|
|
func TestLocalReconciliationRejectsInvalidSession(t *testing.T) {
|
|
for _, mode := range []string{"expired", "cancelled", "changed-admin", "closed", "reset-stage", "wrong-id", "wrong-token", "changed-peer", "foreign-session", "replaced-confirmation"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
server, session, journal := localReconciliationFixture(t)
|
|
prepared, err := session.PrepareRecoveryReconciliation(t.Context(), journal.source.Intent.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
switch mode {
|
|
case "expired":
|
|
session.expires = time.Time{}
|
|
case "cancelled":
|
|
cancel()
|
|
case "changed-admin":
|
|
server.config.Admin.Email = "changed@example.test"
|
|
case "closed":
|
|
session.Close()
|
|
case "reset-stage":
|
|
session.stage = 1
|
|
case "wrong-id":
|
|
prepared.DispositionID = journal.source.Intent.ID
|
|
case "wrong-token":
|
|
prepared.Token = strings.Repeat("a", 64)
|
|
case "changed-peer":
|
|
session.peer.Identity = "uid:123"
|
|
case "foreign-session":
|
|
session = localDispositionNewSession(t, server)
|
|
case "replaced-confirmation":
|
|
if _, err := session.PrepareRecoveryReconciliation(ctx, journal.source.Intent.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := session.ExecuteRecovery(ctx, prepared.DispositionID, prepared.Token); err == nil {
|
|
t.Fatal("无效会话执行了重新核查")
|
|
}
|
|
if _, err := session.ExecuteRecovery(t.Context(), prepared.DispositionID, prepared.Token); err == nil {
|
|
t.Fatal("失败确认被重放")
|
|
}
|
|
if journal.begins != 0 || journal.finishes != 0 || session.recoveryConfirmation.DispositionID() != "" {
|
|
t.Fatal("无效会话写入了审计或保留了确认")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLocalReconciliationRejectsMissingFilesAndQueriesNewAudit(t *testing.T) {
|
|
server, session, journal := localReconciliationFixture(t)
|
|
prepared, err := session.PrepareRecoveryReconciliation(t.Context(), journal.source.Intent.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := session.ExecuteRecovery(t.Context(), prepared.DispositionID, prepared.Token)
|
|
if err != nil || result.Status != "rejected" || result.SourceDispositionID != journal.source.Intent.ID {
|
|
t.Fatalf("缺失文件仍被放行:%+v %v", result, err)
|
|
}
|
|
if journal.item.Intent.Operator != "uid:0" || journal.begins != 1 || journal.finishes != 1 || journal.source.Status != "uncertain" {
|
|
t.Fatal("身份或历史记录被改变")
|
|
}
|
|
next := localDispositionNewSession(t, server)
|
|
queried, err := next.GetRecoveryDisposition(t.Context(), result.ID)
|
|
if err != nil || queried != result {
|
|
t.Fatal("重连未查到关联审计", err)
|
|
}
|
|
}
|