package host import ( "crypto/ed25519" "errors" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/historyreadback" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14HistoryReadbackProduction(t *testing.T) { for _, fault := range []string{"valid", "projection unavailable", "history unsynced", "source changed", "key unavailable", "wrong instance", "pending original"} { t.Run(fault, func(t *testing.T) { e, units, checks, source, signedPub := signedRecoveryFixture(t) rt, err := e.root.reg.Get(observedHost) if err != nil { t.Fatal(err) } h := seedHostRootHistory(t, *rt) pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } _, evidenceKey, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } now := time.Now().UTC() enrollment := hostfloor.Enrollment{Box: "box", Enrollment: strings.Repeat("e", 32), InstanceID: "i-box"} directory := hostactors.Directory{Store: e.store, PublicKey: pub, Bucket: "bucket"} roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Boxes: map[string]hostactors.Box{"box": {Enrollment: enrollment.Enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active", CensusPublicKey: evidenceKey.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} wire, err := release.SignRootEnvelope(release.RootSigner(key), roster) if err != nil { t.Fatal(err) } if _, err := e.store.Put(t.Context(), hostactors.Key, wire, ""); err != nil { t.Fatal(err) } b := release.HistoryBinding{Account: rt.Account, App: rt.App, Host: rt.Host} scope := historyreadback.Scope{Bucket: directory.Bucket, Box: enrollment.Box, Enrollment: enrollment.Enrollment, Binding: b} q, err := historyreadback.RequestStatus(t.Context(), directory, release.RootSigner(key), scope, now) if err != nil { t.Fatal(err) } projection := &rollbackMemory{files: map[string][]byte{}} // A public projection can neither supply the signed snapshot nor repair it. projection.files[release.HistoryStatusKey(b.Account, b.App)] = []byte(`{"app_complete":true,"identity":"forged"}`) responder := historyReadbackPublisher{directory: directory, enrollment: enrollment, key: func() (ed25519.PrivateKey, error) { if fault == "key unavailable" { return nil, os.ErrPermission } return evidenceKey, nil }, now: func() time.Time { return now }} if fault == "wrong instance" { responder.enrollment.InstanceID = "i-other" } if fault == "history unsynced" { h.Store = &readbackFailStore{Store: h.Store} } if fault == "source changed" { source.openFile = func(string) (*os.File, error) { return nil, os.ErrPermission } } if fault == "pending original" { n := namedUnitRecovery{source: source, pub: signedPub, readLive: os.Readlink} if _, err := n.recover(t.Context(), rt.Host, checks, func(string) string { return "5s" }, e.root.reg); err != nil { t.Fatal(err) } disk, err := checks.attempts() if err != nil { t.Fatal(err) } pending, err := readRollbackAttempt(disk, rt.Host) if err != nil { t.Fatal(err) } pending.Settled = false if err := writeRollbackState(disk, recoveryResultKey(pending), pending); err != nil { t.Fatal(err) } } m := rootHistoryMaintenance{history: h, store: e.store, source: source, checks: checks, pub: func() (ed25519.PublicKey, error) { return pub, nil }, now: func() time.Time { return now }, projection: func() (rootstate.Store, error) { if fault == "projection unavailable" { return nil, os.ErrPermission } return projection, nil }, readback: responder.publish} before := len(units.effects) _ = m.run(t.Context(), b) receipt, err := historyreadback.ReadStatus(t.Context(), directory, q, now) switch fault { case "valid", "projection unavailable": if err != nil { t.Fatal(err) } if receipt.Status.Identity == "forged" || len(receipt.Status.Hosts) != 1 || receipt.Status.Hosts[0].Source == "" || receipt.Status.RecoverySources == nil { t.Fatalf("bad private readback: %+v", receipt) } case "pending original": if err != nil { t.Fatal(err) } if receipt.Status.RecoverySources == nil || len(receipt.Status.RecoverySources.Unresolved) != 1 { t.Fatal("pending original hidden") } default: if err == nil { t.Fatal("unavailable evidence returned authenticated status") } if _, _, err := e.store.Get(t.Context(), historyreadback.ResponseKey(q)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("response on failure: %v", err) } } if len(units.effects) != before { t.Fatal("readback changed service effects") } }) } } type readbackFailStore struct{ rootstate.Store } func (s *readbackFailStore) Write(string, []byte) error { return os.ErrPermission }