// internal/appconfig/generation_test.go package appconfig import ( "context" "encoding/hex" "encoding/json" "errors" "os" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // genEtag reads the app's generation etag, "" for absent — the exact // observation the agent's fast loop makes, so a test that asserts on it is // asserting on the signal itself rather than on this package's internals. func genEtag(t *testing.T, s release.Store, acct, app string) string { t.Helper() _, etag, err := s.Get(context.Background(), GenKey(acct, app)) if err != nil { if errors.Is(err, os.ErrNotExist) { return "" } t.Fatalf("read generation: %v", err) } return etag } func TestTouchGenerationCreatesAndMoves(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := TouchGeneration(ctx, s, "bab", "keymail"); err != nil { t.Fatalf("first TouchGeneration: %v", err) } raw, first, err := s.Get(ctx, GenKey("bab", "keymail")) if err != nil { t.Fatalf("Get after first touch: %v", err) } var g Generation if err := json.Unmarshal(raw, &g); err != nil { t.Fatalf("decode generation: %v", err) } if g.Schema != 1 { t.Fatalf("Schema = %d, want 1", g.Schema) } if _, err := time.Parse(time.RFC3339, g.TouchedAt); err != nil { t.Fatalf("TouchedAt %q is not RFC3339: %v", g.TouchedAt, err) } if nonce, err := hex.DecodeString(g.Nonce); err != nil || len(nonce) == 0 { t.Fatalf("Nonce = %q, want nonempty hex (decode err %v)", g.Nonce, err) } // The etag is the whole point: a second touch must be a DIFFERENT // object, or the loop watching it sees nothing. DirStore's etag is // content-derived, so this also pins that each touch's bytes differ. if err := TouchGeneration(ctx, s, "bab", "keymail"); err != nil { t.Fatalf("second TouchGeneration: %v", err) } if second := genEtag(t, s, "bab", "keymail"); second == first { t.Fatalf("etag %q unchanged after a second touch — the change signal never fires", second) } } // racingPutStore lands a different writer's value at key just before // forwarding the caller's Put, which then fails CAS against the etag the // caller read — the exact race two operators (or an operator and the // console door) running config writes at the same moment produce. type racingPutStore struct { release.Store key string done bool } func (r *racingPutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { if key == r.key && !r.done { r.done = true other := []byte(`{"schema":1,"touched_at":"2026-01-01T00:00:00Z","nonce":"00112233445566778899aabbccddeeff"}`) if _, err := r.Store.Put(ctx, key, other, ifMatch); err != nil { return "", err } } return r.Store.Put(ctx, key, body, ifMatch) } func TestTouchGenerationCASConflictIsSuccess(t *testing.T) { inner := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := TouchGeneration(ctx, inner, "bab", "keymail"); err != nil { t.Fatalf("seed TouchGeneration: %v", err) } racing := &racingPutStore{Store: inner, key: GenKey("bab", "keymail")} if err := TouchGeneration(ctx, racing, "bab", "keymail"); err != nil { t.Fatalf("TouchGeneration on a lost CAS race: %v, want nil (the signal already fired)", err) } if !racing.done { t.Fatal("the racing writer never ran — this test proved nothing") } } // TestTouchGenerationSurfacesRealPutErrors is the other half of the CAS // carve-out: only ErrCASConflict is success. Any other store failure means // nobody touched the generation, so the caller must hear about it. func TestTouchGenerationSurfacesRealPutErrors(t *testing.T) { s := &failingPutStore{Store: release.NewDirStore(t.TempDir()), err: errors.New("bucket is on fire")} if err := TouchGeneration(context.Background(), s, "bab", "keymail"); err == nil { t.Fatal("TouchGeneration swallowed a non-CAS Put error, want it surfaced") } } // failingPutStore fails every Put with a fixed non-CAS error. type failingPutStore struct { release.Store err error } func (f *failingPutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { return "", f.err } func TestTouchGenerationRefusesInvalidNames(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := TouchGeneration(ctx, s, "../etc", "keymail"); err == nil { t.Fatal("TouchGeneration accepted an invalid account") } if err := TouchGeneration(ctx, s, "bab", "../etc"); err == nil { t.Fatal("TouchGeneration accepted an invalid app") } } // TestWritesBumpGeneration is the contract the console door and the CLI both // depend on: EVERY successful config write leaves a moved etag behind, and a // REFUSED write leaves none — a failed write has nothing to converge to, and // a spurious touch would cost the fleet a SyncExec pass for nothing. func TestWritesBumpGeneration(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() envelope := testEnvelope(t) writes := []struct { name string run func() error }{ {"SetEnv", func() error { return SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}) }}, {"SetSecret", func() error { return SetSecret(ctx, s, "bab", "keymail", map[string]string{"SES_SECRET": envelope}) }}, {"Unset", func() error { _, err := Unset(ctx, s, "bab", "keymail", []string{"LOG_LEVEL"}) return err }}, {"SaveEnvironment", func() error { return SaveEnvironment(ctx, s, "bab", "keymail", "staging", map[string]Var{"PORT": {Kind: "env", Value: "8080"}}, nil) }}, {"UnsetEnvironment", func() error { _, err := UnsetEnvironment(ctx, s, "bab", "keymail", "staging", []string{"PORT"}) return err }}, {"SetExecDelivery", func() error { return SetExecDelivery(ctx, s, "bab", "keymail", ExecDeliveryFile) }}, {"Apply", func() error { _, err := Apply(ctx, s, "bab", "keymail", map[string]Var{"A": {Kind: "env", Value: "1"}}, []string{"SES_SECRET"}) return err }}, {"ApplyEnvironment", func() error { _, err := ApplyEnvironment(ctx, s, "bab", "keymail", "staging", map[string]Var{"B": {Kind: "env", Value: "2"}}, nil) return err }}, } for _, w := range writes { before := genEtag(t, s, "bab", "keymail") if err := w.run(); err != nil { t.Fatalf("%s: %v", w.name, err) } if after := genEtag(t, s, "bab", "keymail"); after == before { t.Fatalf("%s left the generation etag at %q — no convergence signal", w.name, before) } } // A REFUSED write: unsetting from an app that has no bundle at all is // applyAt's create-refusal. Nothing landed, so nothing may be signalled. before := genEtag(t, s, "bab", "unconfigured") if _, err := Unset(ctx, s, "bab", "unconfigured", []string{"NOPE"}); err == nil { t.Fatal("Unset on an app with no bundle succeeded, want a refusal") } if after := genEtag(t, s, "bab", "unconfigured"); after != before { t.Fatalf("a refused write bumped the generation (%q -> %q)", before, after) } } // testEnvelope seals a throwaway value so secret-shaped writes in these // tests carry a real envelope rather than a string that only looks like one. func testEnvelope(t *testing.T) string { t.Helper() _, pub, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } pubKey, err := ParsePublicKey(pub, "test") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } envelope, err := Seal(pubKey, Binding{Account: "bab", App: "keymail"}, []byte("shh")) if err != nil { t.Fatalf("Seal: %v", err) } return envelope } // genFailingStore fails Put only for one app's GENERATION key — the shape // TestTouchFailureNeverFailsTheWrite needs: the manifest write must succeed // while the follow-up touch cannot. type genFailingStore struct { release.Store genKey string err error } func (g *genFailingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { if key == g.genKey { return "", g.err } return g.Store.Put(ctx, key, body, ifMatch) } // TestTouchFailureNeverFailsTheWrite pins the spec §1 property touchLogged // exists for: when the manifest write LANDS and only the generation touch // fails, the caller sees success — returning an error would send an operator // re-running a write that is already live, and the real cost of the failed // touch is only that convergence falls back to the hourly pass. func TestTouchFailureNeverFailsTheWrite(t *testing.T) { ctx := context.Background() s := &genFailingStore{ Store: release.NewDirStore(t.TempDir()), genKey: GenKey("bab", "keymail"), err: errors.New("bucket is on fire"), } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"PORT": "8080"}); err != nil { t.Fatalf("SetEnv with a failing generation touch = %v, want nil (the manifest write landed)", err) } m, err := Get(ctx, s, "bab", "keymail") if err != nil || m.Vars["PORT"].Value != "8080" { t.Fatalf("manifest did not land: %v %v", m, err) } if _, _, err := s.Get(ctx, GenKey("bab", "keymail")); !errors.Is(err, os.ErrNotExist) { t.Fatal("generation object exists despite the failing Put — the fixture proves nothing") } }