package host import ( "context" "crypto/sha256" "encoding/hex" "fmt" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) func TestPlan14RootTimerInventory(t *testing.T) { for _, attack := range []string{"unrecorded", "intended", "missing", "omitted-timer", "row identity", "owner", "path", "root", "shape"} { t.Run(attack, func(t *testing.T) { t.Setenv("CARLOS_HOST_CLAIM", "1") f := newRecoveryFixture(t) o := f.read() h := o.Claim.Holder plan14PutRecord(t, f.claims, provision.Record{Schema: 1, Host: o.Host, Account: h.Account, App: h.App, Channel: "stable", CreatedAt: f.now}) _, etag, _ := f.claims.Get(t.Context(), h.Key) journals, err := hostbox.NewJournalService(f.journal) if err != nil { t.Fatal(err) } reg := restartTestRegistry(t) sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", o.Host, h.Incarnation, h.Stamp, "box-1"))) owner := registry.ClaimOwnership{ClaimIncarnation: h.Incarnation, ClaimStamp: h.Stamp, ClaimOwner: hex.EncodeToString(sum[:]), ClaimRowID: "route"} row := registry.Route{Host: o.Host, Account: h.Account, App: h.App, DBPath: provision.NestedDBPath(f.guard.DataDir, o.Host), ClaimOwnership: owner} if err = reg.Upsert(row); err != nil { t.Fatal(err) } if err = reg.UpsertTimer(registry.Timer{Host: o.Host, Name: "daily", Kind: "cron", Spec: "0 0 * * *", Path: "/daily", ClaimOwnership: owner}); err != nil { t.Fatal(err) } timer, err := reg.GetTimer(o.Host, "daily") if err != nil { t.Fatal(err) } if !timer.ClaimOwnership.Valid() { t.Fatal("timer fixture has no ownership") } in := hostbox.StopInventory{Complete: true, Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: "box-1", Enrollment: "en", DirectoryRevision: "1", Entries: []hostbox.StopInventoryEntry{{Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: o.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}}} if err = journals.Inventory.WriteStopInventory(t.Context(), in); err != nil { t.Fatal(err) } g := f.guard g.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) if err = g.EstablishRoot(t.Context(), hostguard.RootInstallation{Artifacts: hostguard.InstallationArtifacts{Schema: 1, StateDirectory: true}, Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: "box-1", Enrollment: "en", Migration: 1, InventoryRevision: 1}, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } actor := hostbox.Actor{Box: "box-1", Enrollment: "en", Revision: 1, Binding: "1"} root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: g, Evidence: f.claims}}, Journals: journals, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Roots: map[string]string{"data": g.DataDir}, Reconcile: hostbox.ReconcileAdapter{Registry: reg, Census: &hostbox.EmptyInstallationCensus{}}} e := hostbox.StopInventoryEntry{Kind: "timer", ArtifactID: "timer", ManagedRoot: "registry", RelativePath: o.Host + "/daily", RowIdentity: timer.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"} switch attack { case "missing", "intended": if err = reg.DeleteTimer(o.Host, "daily"); err != nil { t.Fatal(err) } if attack == "intended" { e.Materialization = "intended" } case "row identity": e.RowIdentity = "other" case "owner": e.Ownership = "other" case "path": e.RelativePath = "other.example/daily" case "root": e.ManagedRoot = "data" case "shape": e.Inode = 1 } if attack != "omitted-timer" { in.Entries = append(in.Entries, e) } _, client := hostbox.RemoteJournals(root.commitRootInventory) _, err = client.WriteExpected(t.Context(), in, hostbox.InventoryExpectation{Revision: 1, RecordKey: h.Key, RecordETag: etag, Target: hostguard.FenceTarget{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Box: actor.Box, Enrollment: actor.Enrollment, ClaimETag: o.ETag, Op: o.Claim.Op.ID, Phase: o.Claim.Op.Phase}}) valid := attack == "intended" if valid && err != nil || !valid && err == nil { t.Fatal(attack, err) } _, revision, _ := journals.Inventory.Snapshot(t.Context(), o.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}) want := uint64(1) if valid { want = 2 } if valid { views, readErr := g.Root.GenerationInstallations(t.Context()) if readErr != nil || len(views) != 1 || views[0].Installation.Artifacts.Timers != "daily" { t.Fatalf("timer obligation not retained: %+v %v", views, readErr) } } if revision != want { t.Fatal(revision, want) } }) } }