package release import ( "os" "path/filepath" "testing" "time" ) func TestPlan14HistoryMigrationStatus(t *testing.T) { for _, fault := range []string{"valid", "missing", "changed source", "malformed", "unsynced"} { t.Run(fault, func(t *testing.T) { h, s, b := rootHistoryFixture(t) if fault == "missing" { os.Remove(filepath.Join(s.dir, historyKey(b))) } if fault == "malformed" { s.Write(historyKey(b), []byte("{")) } if fault == "unsynced" { s.fail = 1 } calls := 0 view, err := h.MigrationStatus(b, func(HistoryBinding) (string, error) { calls++ if fault == "changed source" && calls > 1 { return "new", nil } return "canonical", nil }) if fault == "valid" || fault == "missing" { if err != nil || view.Identity == "" || view.Revision != 1 || len(view.Hosts) != 1 || view.Hosts[0].Source != "canonical" { t.Fatalf("view=%+v err=%v", view, err) } if fault == "missing" && (view.Hosts[0].Revision != 0 || view.Hosts[0].Programs.Complete || view.Hosts[0].RefusalStatus.Complete) { t.Fatal("absence became completeness") } if fault == "valid" && view.Hosts[0].Revision != 1 { t.Fatal("wrong category revision") } } else if err == nil { t.Fatalf("%s projected usable status: %+v", fault, view) } }) } } func TestPlan14HistoryPendingStatus(t *testing.T) { h, s, b, pub, wire, now := reconstructionCrashFixture(t) h.Store = &reconstructionCrashStore{healthyRetryStore: s, stage: "intent", fail: true} resolve := func(HistoryBinding) (string, error) { return "canonical", nil } if err := h.Reconstruct(pub, wire, b.Account, b.App, now, resolve); err == nil { t.Fatal("no crash") } h.Store = s view, err := h.MigrationStatus(b, resolve) if err != nil || !view.Pending || view.PendingDigest == "" || view.PendingGrant == nil || view.PendingGrant.Operation != "crash" { t.Fatalf("pending status: %+v %v", view, err) } if _, err := h.Refusals(b); err == nil { t.Fatal("status removed pending barrier") } projection := &healthyRetryStore{dir: t.TempDir(), fault: os.ErrPermission} if err := h.ProjectMigrationStatus(projection, b, resolve); err != nil { t.Fatal(err) } raw, err := projection.Read(HistoryStatusKey(b.Account, b.App)) if err != nil || len(raw) == 0 { t.Fatalf("projection missing: %v", err) } projection.Write(HistoryStatusKey(b.Account, b.App), []byte(`{"complete":true}`)) if _, err := h.Refusals(b); err == nil { t.Fatal("projection supplied authority") } } func TestPlan14HistoryStatusReplacementDelivery(t *testing.T) { h, s, b, pub, wire, now := reconstructionCrashFixture(t) // Retain a real interrupted intent, then change its canonical source. h.Store = &reconstructionCrashStore{healthyRetryStore: s, stage: "intent", fail: true} if err := h.Reconstruct(pub, wire, b.Account, b.App, now, func(HistoryBinding) (string, error) { return "canonical", nil }); err == nil { t.Fatal("no interruption") } h.Store = s resolve := func(HistoryBinding) (string, error) { return "new-source", nil } view, err := h.MigrationStatus(b, resolve) if err != nil { t.Fatal(err) } g := *view.PendingGrant g.Operation = "replacement-from-status" g.Replaces = view.PendingDigest g.AppRevision, g.BakeRevision = view.AppRevision, view.BakeRevision g.Hosts = nil for _, host := range view.Hosts { g.Hosts = append(g.Hosts, host.HistoryReconstructionHost) } // The deployment signer signs the exact revisions and sources from status. priv, pub := testKeys(t) st := NewDirStore(t.TempDir()) if err := PublishHistoryReconstruction(t.Context(), st, RootSigner(priv), g); err != nil { t.Fatal(err) } if err := h.ReceiveReconstruction(t.Context(), st, pub, b, now, resolve); err != nil { t.Fatal(err) } after, err := h.MigrationStatus(b, resolve) if err != nil || after.Pending || after.Revision != view.Revision+1 || !after.AppComplete || after.BakeComplete == 0 { t.Fatalf("replacement not complete: %+v %v", after, err) } if after.Clock.Holds[channelClockKey(b.Account, b.App, "stable")].Window != time.Hour { t.Fatal("replacement lost intended hold") } g.Operation = "stale-revision" stale, err := SignRootEnvelope(RootSigner(priv), g) if err != nil { t.Fatal(err) } if err := h.Reconstruct(pub, stale, b.Account, b.App, now, resolve); err == nil { t.Fatal("stale projected revision accepted") } }