package appconfig import ( "context" "crypto/sha256" "encoding/hex" "errors" "os" "strings" "testing" "github.com/carlosframework/platform/internal/hostwrite" ) // recordingWork is a claim selection stand-in: it records each identity it // is asked to write under and runs the effect only while open. type recordingWork struct { ids []hostwrite.Identity shut bool } func (w *recordingWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error { w.ids = append(w.ids, id) if w.shut { return hostwrite.ErrUnavailable } return fn() } // TestRenderHeldUnderWritesThroughTheClaimSelection (moves Plan 3 Task 5, // H13): with CARLOS_HOST_CLAIM on, the held render writes the config file // through the work its caller selects, after the merged versions are known // (the work is chosen from them), and acks the move as RenderHeld does. func TestRenderHeldUnderWritesThroughTheClaimSelection(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() work := &recordingWork{} var seen HeldRender hr, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, func(pre HeldRender) (hostwrite.Work, error) { seen = pre return work, nil }) if err != nil { t.Fatal(err) } if seen.BundleETags["default"] != def || seen.BundleETags["environment"] != env || seen.OverlayETag != overlay { t.Fatalf("the work was chosen from %+v", seen) } if hr.BundleETags["default"] != def || hr.OverlayETag != overlay || hr.SHA256 == "" || hr.Path != ConfigEnvPath(dd, rt) { t.Fatalf("render: %+v", hr) } if len(work.ids) == 0 { t.Fatal("the file was not written through the selection") } if _, err := os.Stat(hr.Path); err != nil { t.Fatal(err) } if a := readAck(t, ctx, s); a.MoveOp != heldOp || a.FileSHA256 != hr.SHA256 || a.Box != heldBox { t.Fatalf("ack: %+v", a) } } // A refused selection writes nothing, and a missing work is refused before // any write; the ownership checks are RenderHeld's. func TestRenderHeldUnderRefusals(t *testing.T) { ctx, s, _, dd, privB64, _ := execKit(t) priv, err := ParsePrivateKey(privB64, "test") if err != nil { t.Fatal(err) } heldFixture(t, ctx, s) t.Setenv("CARLOS_HOST_CLAIM", "1") rt := landingRow() shut := &recordingWork{shut: true} if _, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) { return shut, nil }); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("a refused selection: %v", err) } selectErr := errors.New("no selection") if _, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) { return nil, selectErr }); !errors.Is(err, selectErr) { t.Fatalf("a failed selection: %v", err) } if _, err := RenderHeldUnder(ctx, s, priv, dd, rt, heldOp, heldBox, nil); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("no selection: %v", err) } ready := rt ready.Status = "ready" if _, err := RenderHeldUnder(ctx, s, priv, dd, ready, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) { return &recordingWork{}, nil }); !errors.Is(err, ErrNotHeld) { t.Fatalf("a row SyncExec owns: %v", err) } if _, err := os.Stat(ConfigEnvPath(dd, rt)); !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) } } // 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) } }) }