| index 002944b63..0d2347efc 100644 |
| --- a/internal/appconfig/renderheld_claim_test.go |
| +++ b/internal/appconfig/renderheld_claim_test.go |
| @@ -2,8 +2,11 @@ package appconfig |
| |
| import ( |
| "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| "errors" |
| "os" |
| + "strings" |
| "testing" |
| |
| "github.com/carlosframework/platform/internal/hostwrite" |
| @@ -97,3 +100,120 @@ func TestRenderHeldUnderRefusals(t *testing.T) { |
| t.Fatalf("an ack was written: %v", err) |
| } |
| } |
| + |
| +// orderedWork is a claim selection stand-in that records, in log, each |
| +// write it lets through. |
| +type orderedWork struct{ log *[]string } |
| + |
| +func (w orderedWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error { |
| + *w.log = append(*w.log, "write") |
| + return fn() |
| +} |
| + |
| +// TestRenderHeldUnderBeforeJournalsBeforeTheWrite (moves after claim Plan |
| +// 12, Task 7; platform-1e's answer to Q2.2): the held render under the claim |
| +// keeps carry (c). The work is chosen from the merged versions, then before |
| +// learns the sha256 of exactly the bytes the render places, and only then |
| +// does the selection write them. A refusal from before writes nothing, sends |
| +// no ack and never enters the selection; a missing before or work is refused |
| +// before anything is read or written; the ownership checks are RenderHeld's. |
| +func TestRenderHeldUnderBeforeJournalsBeforeTheWrite(t *testing.T) { |
| + ctx, s, _, dd, privB64, _ := execKit(t) |
| + priv, err := ParsePrivateKey(privB64, "test") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + def, env, overlay := heldFixture(t, ctx, s) |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + rt := landingRow() |
| + path := ConfigEnvPath(dd, rt) |
| + noAck := func(t *testing.T) { |
| + t.Helper() |
| + if _, err := os.Stat(path); !os.IsNotExist(err) { |
| + t.Fatalf("a file was written: %v", err) |
| + } |
| + if _, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost)); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("an ack was written: %v", err) |
| + } |
| + } |
| + |
| + t.Run("refusals", func(t *testing.T) { |
| + pick := func(HeldRender) (hostwrite.Work, error) { return &recordingWork{}, nil } |
| + if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, pick, nil); !errors.Is(err, hostwrite.ErrUnavailable) { |
| + t.Fatalf("no before: %v", err) |
| + } |
| + if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, nil, func(string) error { return nil }); !errors.Is(err, hostwrite.ErrUnavailable) { |
| + t.Fatalf("no work: %v", err) |
| + } |
| + ready := rt |
| + ready.Status = "ready" |
| + if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, ready, heldOp, heldBox, pick, func(string) error { return nil }); !errors.Is(err, ErrNotHeld) { |
| + t.Fatalf("a row SyncExec owns: %v", err) |
| + } |
| + other := rt |
| + other.HoldOp = "mv-other" |
| + if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, other, heldOp, heldBox, pick, func(string) error { return nil }); !errors.Is(err, ErrNotHeld) { |
| + t.Fatalf("a row another op holds: %v", err) |
| + } |
| + noAck(t) |
| + }) |
| + |
| + t.Run("a refusing journal writes nothing", func(t *testing.T) { |
| + var log []string |
| + refused := errors.New("the journal refused") |
| + _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) { |
| + log = append(log, "select") |
| + return orderedWork{&log}, nil |
| + }, func(string) error { |
| + log = append(log, "journal") |
| + return refused |
| + }) |
| + if !errors.Is(err, refused) { |
| + t.Fatalf("a refusing journal: %v", err) |
| + } |
| + if strings.Join(log, ",") != "select,journal" { |
| + t.Fatalf("order %v: the selection wrote after a refused journal", log) |
| + } |
| + noAck(t) |
| + }) |
| + |
| + t.Run("the journal comes before the write", func(t *testing.T) { |
| + var ( |
| + log []string |
| + seen HeldRender |
| + told string |
| + ) |
| + hr, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, func(pre HeldRender) (hostwrite.Work, error) { |
| + log = append(log, "select") |
| + seen = pre |
| + return orderedWork{&log}, nil |
| + }, func(sha string) error { |
| + log = append(log, "journal") |
| + if _, err := os.Stat(path); !os.IsNotExist(err) { |
| + t.Errorf("the file was there before the journal: %v", err) |
| + } |
| + told = sha |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(log) < 3 || log[0] != "select" || log[1] != "journal" || log[2] != "write" { |
| + t.Fatalf("order %v, want the selection, then the journal, then the writes", log) |
| + } |
| + if seen.BundleETags["default"] != def || seen.BundleETags["environment"] != env || seen.OverlayETag != overlay { |
| + t.Fatalf("the work was chosen from %+v", seen) |
| + } |
| + body, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + sum := sha256.Sum256(body) |
| + if want := hex.EncodeToString(sum[:]); told != want || hr.SHA256 != want { |
| + t.Fatalf("journal %q, render %q, file %q", told, hr.SHA256, want) |
| + } |
| + if a := readAck(t, ctx, s); a.MoveOp != heldOp || a.FileSHA256 != told || a.Box != heldBox { |
| + t.Fatalf("ack: %+v", a) |
| + } |
| + }) |
| +} |