package release import ( "context" "encoding/json" "errors" "github.com/carlosframework/platform/internal/rootstate" "os" "strings" "testing" ) func generationEvidence() GenerationEvidence { return GenerationEvidence{Box: "box", Enrollment: "enrollment", Boot: "boot", Migration: 1, Installations: strings.Repeat("a", 64), Launches: strings.Repeat("b", 64), Kernel: strings.Repeat("c", 64)} } func TestPlan14GenerationCompleteness(t *testing.T) { for _, attack := range []string{"none", "missing-verifier", "kernel-unknown", "empty-evidence", "wrong-box", "wrong-enrollment", "changed-reservations", "corrupt-marker", "sync"} { t.Run(attack, func(t *testing.T) { g, s, b := generationFixture(t) if _, err := g.Reserve(b); err != nil { t.Fatal(err) } records, err := g.Reservations(t.Context()) if err != nil { t.Fatal(err) } verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { e := generationEvidence() switch attack { case "kernel-unknown": return e, os.ErrPermission case "empty-evidence": e.Kernel = "" case "wrong-box": e.Box = "other" case "wrong-enrollment": e.Enrollment = "other" case "changed-reservations": b.Attempt = "another" if _, err := g.Reserve(b); err != nil { t.Fatal(err) } case "sync": s.fail = 2 } return e, nil } if attack == "missing-verifier" { verify = nil } if attack == "corrupt-marker" { if err := s.Write(generationCompletenessKey, []byte(`{}`)); err != nil { t.Fatal(err) } } _, err = g.Certify(t.Context(), records, verify) if attack != "none" { if err == nil { t.Fatal("incomplete sources certified") } return } if err != nil { t.Fatal(err) } c, err := g.Completeness() if err != nil || !c.Complete || c.Revision != 1 { t.Fatalf("certificate: %+v %v", c, err) } again, err := g.Certify(t.Context(), records, verify) if err != nil || !again.Complete || again.Revision <= c.Revision || again.Reservations != c.Reservations { t.Fatalf("recheck did not recertify: %+v %v", again, err) } }) } } func TestPlan14GenerationCompletenessInvalidation(t *testing.T) { for _, change := range []string{"reserve", "started", "closed", "retired"} { t.Run(change, func(t *testing.T) { g, _, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { return generationEvidence(), nil } records, err := g.Reservations(t.Context()) if err != nil { t.Fatal(err) } first, err := g.Certify(t.Context(), records, verify) if err != nil { t.Fatal(err) } switch change { case "reserve": b.Attempt = "new" _, err = g.Reserve(b) case "started": err = g.Started(r, GenerationProcess{PID: 100, TenantPID: 101, Start: "1", TenantStart: "2", Group: "system.slice/unit", Invocation: "invoke", GroupIdentity: b.Executable}) default: err = g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { if change == "closed" { return "", os.ErrPermission } return strings.Repeat("d", 64), nil }) } if err != nil && change != "closed" { t.Fatal(err) } c, err := g.Completeness() if err != nil || c.Complete || c.Revision <= first.Revision { t.Fatalf("mutation left certificate usable: %+v %v", c, err) } records, err = g.Reservations(t.Context()) if err != nil { t.Fatal(err) } next, err := g.Certify(t.Context(), records, verify) if err != nil || !next.Complete || next.Reservations == first.Reservations || next.Revision <= c.Revision { t.Fatalf("new revision: %+v %v", next, err) } }) } } func TestPlan14GenerationCompletenessDurability(t *testing.T) { g, s, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } records, err := g.Reservations(t.Context()) if err != nil { t.Fatal(err) } verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { return generationEvidence(), nil } c, err := g.Certify(t.Context(), records, verify) if err != nil { t.Fatal(err) } s.fail = 1 if _, err = g.Completeness(); err == nil { t.Fatal("visible certificate bypassed sync") } g.Store = &generationCompletenessFault{Store: s} b.Attempt = "new" if _, err = g.Reserve(b); err == nil { t.Fatal("unsynced invalidation allowed reserve") } if _, err = g.Read(b); !errors.Is(err, os.ErrNotExist) { t.Fatal("reservation written before invalidation", err) } raw, err := s.Read(generationCompletenessKey) if err != nil { t.Fatal(err) } var pending GenerationCompleteness if err = json.Unmarshal(raw, &pending); err != nil || pending.Complete { t.Fatal("invalidation not visible", err) } reopened := &RootGenerations{Store: s} if _, err = reopened.Reserve(b); err != nil { t.Fatal(err) } if got, err := reopened.Read(r.Binding); err != nil || got != r { t.Fatal("old obligation changed", err) } records, err = reopened.Reservations(t.Context()) if err != nil { t.Fatal(err) } next, err := reopened.Certify(t.Context(), records, verify) if err != nil || next.Revision <= c.Revision { t.Fatal("recovery did not advance revision", err) } } type generationCompletenessFault struct{ rootstate.Store } func (s *generationCompletenessFault) Write(k string, b []byte) error { if err := s.Store.Write(k, b); err != nil { return err } var c GenerationCompleteness if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil && !c.Complete { return os.ErrPermission } return nil } func TestPlan14GenerationCompletenessPinConsumer(t *testing.T) { f := newReuseFixture(t) g, r := generationPinFixture(t, f.opt, "bab", "myapp", "v2") fail := false calls := 0 source := RootGenerationPins{Ledger: g, Reconcile: func(context.Context, []GenerationReservation) (GenerationEvidence, error) { calls++ if fail { return GenerationEvidence{}, os.ErrPermission } return generationEvidence(), nil }} pins, err := source.StagedPins(t.Context(), f.opt) if err != nil { t.Fatal(err) } if pinned, err := pins.Refers(r.Binding.Path); err != nil || !pinned { t.Fatal("complete snapshot dropped pending pin", err) } c, err := g.Completeness() if err != nil || !c.Complete || calls != 1 { t.Fatalf("pin reader omitted certificate: %+v %v calls=%d", c, err, calls) } fail = true if _, err := source.StagedPins(t.Context(), f.opt); err == nil { t.Fatal("cached completeness survived unknown kernel") } c, err = g.Completeness() if err != nil || c.Complete || calls != 2 { t.Fatalf("failed scan retained certificate: %+v %v calls=%d", c, err, calls) } fail = false if _, err := source.StagedPins(t.Context(), f.opt); err != nil { t.Fatal(err) } } func TestPlan14GenerationCompletenessCertificateSyncFailure(t *testing.T) { g, s, b := generationFixture(t) if _, err := g.Reserve(b); err != nil { t.Fatal(err) } records, err := g.Reservations(t.Context()) if err != nil { t.Fatal(err) } g.Store = generationPositiveCertificateFault{Store: s} verify := func(context.Context, []GenerationReservation) (GenerationEvidence, error) { return generationEvidence(), nil } if _, err := g.Certify(t.Context(), records, verify); err == nil { t.Fatal("unsynced certificate returned success") } raw, err := s.Read(generationCompletenessKey) if err != nil { t.Fatal(err) } var visible GenerationCompleteness if err := json.Unmarshal(raw, &visible); err != nil || !visible.Complete { t.Fatal("positive rename was not visible", err) } if _, err := g.Completeness(); err == nil { t.Fatal("unsynced visible record bypassed durability retry") } g.Store = s checked := false if _, err := g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) { checked = true return GenerationEvidence{}, os.ErrPermission }); err == nil || !checked { t.Fatal("reopen reused old evidence") } if _, err := g.Certify(t.Context(), records, verify); err != nil { t.Fatal(err) } } type generationPositiveCertificateFault struct{ rootstate.Store } func (s generationPositiveCertificateFault) Write(k string, b []byte) error { if err := s.Store.Write(k, b); err != nil { return err } var c GenerationCompleteness if k == generationCompletenessKey && json.Unmarshal(b, &c) == nil && c.Complete { return os.ErrPermission } return nil } func TestPlan14GenerationCompletenessUnknownRevisionAdvances(t *testing.T) { g, _, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } records, err := g.Reservations(t.Context()) if err != nil { t.Fatal(err) } if _, err := g.Certify(t.Context(), records, func(context.Context, []GenerationReservation) (GenerationEvidence, error) { return generationEvidence(), nil }); err != nil { t.Fatal(err) } if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { return "", os.ErrPermission }); err == nil { t.Fatal("unknown kernel retired") } before, err := g.Completeness() if err != nil || before.Complete { t.Fatal("missing incomplete state", err) } if err := g.Retire(t.Context(), r, func(context.Context, GenerationReservation) error { return nil }, func(context.Context, GenerationReservation) (string, error) { return strings.Repeat("d", 64), nil }); err != nil { t.Fatal(err) } after, err := g.Completeness() if err != nil || after.Complete || after.Revision <= before.Revision { t.Fatalf("retirement failed to advance unknown revision: before=%+v after=%+v err=%v", before, after, err) } }