| index a6ad3708a..095a689e8 100644 |
| --- a/internal/edge/staticdigest_test.go |
| +++ b/internal/edge/staticdigest_test.go |
| @@ -2,11 +2,14 @@ package edge |
| |
| import ( |
| "context" |
| + "crypto/ed25519" |
| "io" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "path/filepath" |
| + "slices" |
| + "strings" |
| "testing" |
| |
| "github.com/carlosframework/platform/internal/registry" |
| @@ -134,3 +137,161 @@ func TestStaticWithoutDigestsServesAsBefore(t *testing.T) { |
| t.Fatalf("legacy site = %q %q", b, v) |
| } |
| } |
| + |
| +// prodStaticEdge is an Edge whose static serving is configured the way the |
| +// edge process configures it, through enableStaticFromEnv: the store and key |
| +// from the environment, and the ratchet built there from dataDir and |
| +// CARLOS_OPT_DIR (Astra r1 #6). The pointer cache is off, so every request |
| +// re-reads the pointer. |
| +type prodStaticEdge struct { |
| + store release.Store |
| + priv ed25519.PrivateKey |
| + data string |
| + srv *httptest.Server |
| +} |
| + |
| +func newProdStaticEdge(t *testing.T, require bool, seed func(release.Store, ed25519.PrivateKey)) *prodStaticEdge { |
| + t.Helper() |
| + storeDir := filepath.Join(t.TempDir(), "store") |
| + store := release.NewDirStore(storeDir) |
| + seedB64, pubB64, err := release.GenerateKey() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + priv, err := release.ParseSigningKey(seedB64, "test") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + seed(store, priv) |
| + t.Setenv("CARLOS_RELEASE_PUBKEY", pubB64) |
| + t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") |
| + t.Setenv("CARLOS_DEPLOYMENT_DIR", storeDir) |
| + t.Setenv("CARLOS_OPT_DIR", t.TempDir()) |
| + flag := "" |
| + if require { |
| + flag = "1" |
| + } |
| + t.Setenv(release.RequirePointerDigestEnv, flag) |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{Host: "site.example.com", Kind: "static", App: "site", Channel: "canary/x", Status: "ready"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + data := t.TempDir() |
| + if err := enableStaticFromEnv(e, data); err != nil { |
| + t.Fatal(err) |
| + } |
| + e.SetStaticRecheck(0) |
| + if err := e.Reload(); err != nil { |
| + t.Fatal(err) |
| + } |
| + srv := httptest.NewServer(e) |
| + t.Cleanup(srv.Close) |
| + return &prodStaticEdge{store: store, priv: priv, data: data, srv: srv} |
| +} |
| + |
| +func (p *prodStaticEdge) get(t *testing.T) (int, string, string) { |
| + t.Helper() |
| + req, err := http.NewRequest("GET", p.srv.URL+"/", nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + req.Host = "site.example.com" |
| + res, err := http.DefaultClient.Do(req) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer res.Body.Close() |
| + b, _ := io.ReadAll(res.Body) |
| + return res.StatusCode, string(b), res.Header.Get("X-Carlos-Version") |
| +} |
| + |
| +// extracted lists the site trees under the edge's static cache. |
| +func (p *prodStaticEdge) extracted(t *testing.T) []string { |
| + t.Helper() |
| + entries, err := os.ReadDir(filepath.Join(p.data, "static", release.OpsAccount, "site")) |
| + if err != nil && !os.IsNotExist(err) { |
| + t.Fatal(err) |
| + } |
| + var out []string |
| + for _, e := range entries { |
| + if e.IsDir() && !strings.HasPrefix(e.Name(), ".") { |
| + out = append(out, e.Name()) |
| + } |
| + } |
| + return out |
| +} |
| + |
| +func (p *prodStaticEdge) promote(t *testing.T, v, page string) { |
| + t.Helper() |
| + shipSite(t, p.store, "site", v, map[string]string{"index.html": page}) |
| + if err := release.Promote(context.Background(), p.store, p.priv, release.OpsAccount, "site", "canary/x", v); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +// Astra r1 #6: the static handler, configured as production configures it, |
| +// refuses a replayed legacy pointer once it has served a pointer with |
| +// digests. The saved v1 pointer comes back with v1's name reshipped under |
| +// other bytes; the edge keeps serving v2 and extracts nothing new. The |
| +// control is the same legacy pointer served on first sight. |
| +func TestEdgeStaticRefusesAReplayedLegacyPointer(t *testing.T) { |
| + p := newProdStaticEdge(t, false, func(release.Store, ed25519.PrivateKey) {}) |
| + ctx := context.Background() |
| + p.promote(t, "v1", "home v1") // no marker: a legacy pointer |
| + key := release.ChannelKey(release.OpsAccount, "site", "canary/x") |
| + saved, _, err := p.store.Get(ctx, key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if code, b, v := p.get(t); code != http.StatusOK || b != "home v1" || v != "v1" { |
| + t.Fatalf("control: the legacy pointer on first sight = %d %q %q", code, b, v) |
| + } |
| + if err := release.WriteDigestMarker(ctx, p.store, release.RootSigner(p.priv), release.OpsAccount, "test"); err != nil { |
| + t.Fatal(err) |
| + } |
| + p.promote(t, "v2", "home v2") // with digests: the edge marks the channel |
| + if code, b, v := p.get(t); code != http.StatusOK || b != "home v2" || v != "v2" { |
| + t.Fatalf("the digest pointer = %d %q %q", code, b, v) |
| + } |
| + before := p.extracted(t) |
| + if err := release.DeleteVersion(ctx, p.store, release.OpsAccount, "site", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + shipSite(t, p.store, "site", "v1", map[string]string{"index.html": "planted"}) |
| + _, etag, _ := p.store.Get(ctx, key) |
| + if _, err := p.store.Put(ctx, key, saved, etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + if code, b, v := p.get(t); code != http.StatusOK || b != "home v2" || v != "v2" { |
| + t.Fatalf("after the replay = %d %q %q; want v2 still served", code, b, v) |
| + } |
| + if after := p.extracted(t); !slices.Equal(before, after) { |
| + t.Fatalf("the replay extracted a tree: before %v, after %v", before, after) |
| + } |
| +} |
| + |
| +// CARLOS_REQUIRE_POINTER_DIGEST through the edge's own ratchet construction: |
| +// a fresh box with the flag on refuses a legacy pointer for a channel it has |
| +// never seen, so nothing is extracted or served; with the flag off (the |
| +// control) the same pointer serves. |
| +func TestEdgeStaticRequireFlagOnAFreshBox(t *testing.T) { |
| + for _, on := range []bool{false, true} { |
| + p := newProdStaticEdge(t, on, func(s release.Store, priv ed25519.PrivateKey) { |
| + shipSite(t, s, "site", "v1", map[string]string{"index.html": "home v1"}) |
| + if err := release.Promote(context.Background(), s, priv, release.OpsAccount, "site", "canary/x", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + code, b, _ := p.get(t) |
| + switch { |
| + case on && (code == http.StatusOK || len(p.extracted(t)) != 0): |
| + t.Fatalf("flag on: a fresh box served a legacy pointer: %d %q, extracted %v", code, b, p.extracted(t)) |
| + case !on && (code != http.StatusOK || b != "home v1"): |
| + t.Fatalf("control, flag off: %d %q, want home v1", code, b) |
| + } |
| + } |
| +} |