package edge import ( "context" "crypto/ed25519" "io" "net/http" "net/http/httptest" "os" "path/filepath" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func siteDirOf(t *testing.T, store release.Store, app, version string) string { t.Helper() raw, _, err := store.Get(context.Background(), release.ManifestKey(release.OpsAccount, app, version)) if err != nil { t.Fatal(err) } return siteDirName(version, release.ManifestDigest(raw)) } func digestSite(t *testing.T, ratchet *release.DigestRatchet) (release.Store, *staticSite, *httptest.Server, func(string, map[string]string)) { t.Helper() store := release.NewDirStore(filepath.Join(t.TempDir(), "store")) priv, pub := testKeys(t) if err := release.WriteDigestMarker(context.Background(), store, release.RootSigner(priv), release.OpsAccount, "test"); err != nil { t.Fatal(err) } promote := func(v string, files map[string]string) { shipSite(t, store, "site", v, files) if err := release.Promote(context.Background(), store, priv, release.OpsAccount, "site", "canary/x", v); err != nil { t.Fatal(err) } } promote("v1", map[string]string{"index.html": "home v1"}) rt := registry.Route{Host: "site.example.com", Kind: "static", App: "site", Channel: "canary/x", Status: "ready"} h := newStaticHandler(rt, store, pub, filepath.Join(t.TempDir(), "cache")).(*staticSite) h.recheck = 0 h.ratchet = ratchet srv := httptest.NewServer(h) t.Cleanup(srv.Close) return store, h, srv, promote } func getBody(t *testing.T, url string) (string, string) { t.Helper() res, err := http.Get(url) if err != nil { t.Fatal(err) } defer res.Body.Close() b, _ := io.ReadAll(res.Body) return string(b), res.Header.Get("X-Carlos-Version") } // Spec §8: a static site whose version name is reused, under a pointer with // digests, is refused before extraction. func TestStaticRefusesAReusedNameBeforeExtraction(t *testing.T) { store, h, srv, promote := digestSite(t, nil) if b, v := getBody(t, srv.URL+"/"); b != "home v1" || v != "v1" { t.Fatalf("first serve = %q %q", b, v) } saved, _, _ := store.Get(context.Background(), release.ChannelKey(release.OpsAccount, "site", "canary/x")) promote("v2", map[string]string{"index.html": "home v2"}) if b, _ := getBody(t, srv.URL+"/"); b != "home v2" { t.Fatalf("v2 not served: %q", b) } if err := release.DeleteVersion(context.Background(), store, release.OpsAccount, "site", "v1"); err != nil { t.Fatal(err) } shipSite(t, store, "site", "v1", map[string]string{"index.html": "planted"}) _, etag, _ := store.Get(context.Background(), release.ChannelKey(release.OpsAccount, "site", "canary/x")) if _, err := store.Put(context.Background(), release.ChannelKey(release.OpsAccount, "site", "canary/x"), saved, etag); err != nil { t.Fatal(err) } if b, v := getBody(t, srv.URL+"/"); b == "planted" || v != "v2" { t.Fatalf("after the replay = %q %q; want v2 still served", b, v) } if _, err := os.Stat(filepath.Join(h.appDir(), siteDirOf(t, store, "site", "v1"))); err == nil { t.Fatal("the reused name's archive was extracted") } } // Spec §8: a cached tree from other bytes is not served. func TestStaticNeverServesACachedTreeFromOtherBytes(t *testing.T) { store, h, srv, _ := digestSite(t, nil) other := filepath.Join(h.appDir(), siteDirName("v1", release.ManifestDigest([]byte("other manifest")))) if err := os.MkdirAll(other, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(other, "index.html"), []byte("stale tree"), 0o644); err != nil { t.Fatal(err) } if b, _ := getBody(t, srv.URL+"/"); b != "home v1" { t.Fatalf("served %q from a tree keyed by another digest", b) } if _, err := os.Stat(filepath.Join(h.appDir(), siteDirOf(t, store, "site", "v1"), "index.html")); err != nil { t.Fatalf("not extracted under its own digest: %v", err) } } // Review Focus 5. func TestStaticNeverReusesALegacyCacheDir(t *testing.T) { _, h, srv, _ := digestSite(t, nil) legacy := filepath.Join(h.appDir(), "v1") if err := os.MkdirAll(legacy, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(legacy, "index.html"), []byte("pre-upgrade tree"), 0o644); err != nil { t.Fatal(err) } if b, _ := getBody(t, srv.URL+"/"); b != "home v1" { t.Fatalf("served %q from the bare-version directory", b) } } func TestStaticRatchetRefusesALegacyPointerAfterDigests(t *testing.T) { r := release.NewDigestRatchet(t.TempDir(), nil, false) _, _, srv, _ := digestSite(t, r) if _, v := getBody(t, srv.URL+"/"); v != "v1" { t.Fatalf("version %q", v) } if err := r.Admit(release.OpsAccount, &release.ChannelPointer{App: "site", Channel: "canary/x", Version: "v1"}); err == nil { t.Fatal("the edge's ratchet did not arm on the digest-bearing pointer it served") } } func TestStaticWithoutDigestsServesAsBefore(t *testing.T) { _, _, _, srv := staticFixture(t) if b, v := getBody(t, srv.URL+"/"); b != "home v1" || v != "v1" { 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) } } }