Files
Agent-System-Backend/internal/server/recovery_multi_instance_test.go
T
weichen 7229abe50d Fence final run settlement against account disable
Why: A disable could commit after the run manager checked account state but before its final business message was stored.

Evidence: SELF receipts /tmp/iam-u3-final-settle-sqlite.json, /tmp/iam-u3-final-settle-pg.json, /tmp/iam-u3-backend-settle-all.json, /tmp/iam-u3-backend-settle-pg.json, and /tmp/iam-u3-http-two-instance-pg.json.

Impact: The final run transaction locks and rechecks the account, rejects post-disable business output, and permits only an account-denial failure record. Auth-off implicit local ownership remains explicit.

Refs: IAM U3 r1 stages 4-5; Dev feature/iam-u3-account-directory-recovery
2026-10-14 04:48:53 +00:00

125 lines
4.8 KiB
Go

package server
import (
"database/sql"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"agent-system-backend/internal/store"
)
func twinRecoveryStores(t *testing.T) (*store.Store, *store.Store) {
t.Helper()
dsn := os.Getenv("AGENT_PG_TEST_DSN")
if dsn == "" {
path := filepath.Join(t.TempDir(), "shared.db")
first, err := store.OpenSQLite(path)
if err != nil {
t.Fatal(err)
}
second, err := store.OpenSQLite(path)
if err != nil {
first.Close()
t.Fatal(err)
}
t.Cleanup(func() { second.Close(); first.Close() })
return first, second
}
admin, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatal(err)
}
if _, err := admin.Exec(`CREATE EXTENSION IF NOT EXISTS pg_trgm`); err != nil {
t.Fatal(err)
}
schema := "t_" + strings.ToLower(store.NewID("recovery"))
if _, err := admin.Exec(`CREATE SCHEMA ` + schema); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _, _ = admin.Exec(`DROP SCHEMA IF EXISTS ` + schema + ` CASCADE`); admin.Close() })
isolate := store.AppendPGRuntimeParam(dsn, "search_path", schema+",public")
first, err := store.OpenPostgres(isolate, filepath.Join(t.TempDir(), "a-artifacts"))
if err != nil {
t.Fatal(err)
}
second, err := store.OpenPostgres(isolate, filepath.Join(t.TempDir(), "b-artifacts"))
if err != nil {
first.Close()
t.Fatal(err)
}
t.Cleanup(func() { second.Close(); first.Close() })
return first, second
}
func TestRecoveryAcrossTwoHTTPInstancesAndIndependentPools(t *testing.T) {
first, second := twinRecoveryStores(t)
t.Logf("recovery HTTP dialect=%s two independent stores", first.Dialect())
hA := newAuthTestServerOnStore(t, first, t.TempDir()).Handler()
hB := newAuthTestServerOnStore(t, second, t.TempDir()).Handler()
adminCookie := bootAdmin(t, hA, first)
account, err := first.CreatePendingAccount("admin", "two-instance-owner", store.AccountProfile{})
if err != nil {
t.Fatal(err)
}
activation, err := first.InitializePendingAccount(account.ID, account.StateVersion, account.InitializationGeneration)
if err != nil {
t.Fatal(err)
}
setup := do(hB, "POST", "/api/auth/login-key/redeem", `{"key":"`+activation+`"}`, "")
if setup.Code != http.StatusOK {
t.Fatalf("second instance activation redeem: %d %s", setup.Code, setup.Body.String())
}
if done := do(hA, "POST", "/api/auth/password", `{"next":"two-instance-initial"}`, cookieOf(setup)); done.Code != http.StatusOK {
t.Fatalf("first instance initial set: %d %s", done.Code, done.Body.String())
}
login := do(hA, "POST", "/api/auth/login", `{"owner":"two-instance-owner","password":"two-instance-initial"}`, "")
if login.Code != http.StatusOK {
t.Fatalf("first instance login: %d %s", login.Code, login.Body.String())
}
ownerCookie := cookieOf(login)
current, err := second.GetAccount(account.ID)
if err != nil {
t.Fatal(err)
}
proposalResponse := do(hA, "POST", "/api/admin/users/"+account.ID+"/recovery-proposals",
`{"expectedStateVersion":`+jsonNumber(current.StateVersion)+`,"idempotencyKey":"two-instance-proposal"}`, adminCookie)
if proposalResponse.Code != http.StatusOK {
t.Fatalf("proposal: %d %s", proposalResponse.Code, proposalResponse.Body.String())
}
var proposal store.RecoveryProposal
if err := json.Unmarshal(proposalResponse.Body.Bytes(), &proposal); err != nil {
t.Fatal(err)
}
confirmation := do(hB, "POST", "/api/auth/recovery-proposals/"+proposal.ID+"/confirm", `{"expectedVersion":1}`, ownerCookie)
if confirmation.Code != http.StatusOK {
t.Fatalf("second instance confirm: %d %s", confirmation.Code, confirmation.Body.String())
}
var confirmed struct {
Grant string `json:"grant"`
}
if err := json.Unmarshal(confirmation.Body.Bytes(), &confirmed); err != nil || confirmed.Grant == "" {
t.Fatalf("grant response: %v", err)
}
redeemed := do(hA, "POST", "/api/auth/recovery-grants/redeem", `{"key":"`+confirmed.Grant+`"}`, "")
if redeemed.Code != http.StatusOK {
t.Fatalf("first instance grant redeem: %d %s", redeemed.Code, redeemed.Body.String())
}
if completed := do(hB, "POST", "/api/auth/password", `{"next":"two-instance-recovered"}`, cookieOf(redeemed)); completed.Code != http.StatusOK {
t.Fatalf("second instance recovery completion: %d %s", completed.Code, completed.Body.String())
}
if old := do(hB, "GET", "/api/me", "", ownerCookie); old.Code != http.StatusUnauthorized {
t.Fatalf("old proof session survived: %d", old.Code)
}
if fresh := do(hA, "POST", "/api/auth/login", `{"owner":"two-instance-owner","password":"two-instance-recovered"}`, ""); fresh.Code != http.StatusOK {
t.Fatalf("recovered password login: %d %s", fresh.Code, fresh.Body.String())
}
after, err := first.GetAccount(account.ID)
if err != nil || after.CredentialOrigin != store.CredentialOriginOwnerRecovery || after.ID != account.ID {
t.Fatalf("persistent identity or provenance changed: %+v %v", after, err)
}
}