test(boardwalk): cover the build id, the reload banner and the chunk reload guard (quick-261028-rqb)
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package boardwalk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
@@ -530,3 +533,292 @@ func TestBuildSmoke(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hexID = regexp.MustCompile(`^[0-9a-f]{12}$`)
|
||||
|
||||
func mustBuildID(t *testing.T, root fs.FS, info *debug.BuildInfo) string {
|
||||
t.Helper()
|
||||
id, err := BuildID(root, info)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hexID.MatchString(id) {
|
||||
t.Fatalf("build id %q is not 12 lowercase hex characters", id)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func TestBuildID(t *testing.T) {
|
||||
root := buildFixture()
|
||||
base := mustBuildID(t, root, nil)
|
||||
if again := mustBuildID(t, buildFixture(), nil); again != base {
|
||||
t.Fatalf("same tree gave %q and %q", base, again)
|
||||
}
|
||||
|
||||
changed := buildFixture()
|
||||
changed["assets/app.js"] = &fstest.MapFile{Data: []byte(`console.log("acme!")`)}
|
||||
added := buildFixture()
|
||||
added["assets/extra.css"] = &fstest.MapFile{Data: []byte(`body{}`)}
|
||||
renamed := buildFixture()
|
||||
renamed["assets/app2.js"] = renamed["assets/app.js"]
|
||||
delete(renamed, "assets/app.js")
|
||||
for name, tree := range map[string]fstest.MapFS{"changed file": changed, "added file": added, "renamed file": renamed} {
|
||||
if id := mustBuildID(t, tree, nil); id == base {
|
||||
t.Fatalf("%s kept the id %q", name, id)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := BuildID(fstest.MapFS{}, nil); err != nil {
|
||||
t.Fatalf("empty tree: %v", err)
|
||||
}
|
||||
mustBuildID(t, fstest.MapFS{}, nil)
|
||||
}
|
||||
|
||||
// buildInfoFixture is a hand-built build info; tests never depend on the test
|
||||
// binary's real build information for exact values.
|
||||
func buildInfoFixture() *debug.BuildInfo {
|
||||
return &debug.BuildInfo{
|
||||
GoVersion: "go1.26.0",
|
||||
Path: "example.com/acme/cmd/acme",
|
||||
Main: debug.Module{Path: "example.com/acme", Version: "v1.2.3", Sum: "h1:main="},
|
||||
Deps: []*debug.Module{
|
||||
{Path: "example.com/blog", Version: "v0.4.0", Sum: "h1:blog="},
|
||||
{
|
||||
Path: "git.golem15.com/golem15/summercms", Version: "v0.9.0", Sum: "h1:framework=",
|
||||
Replace: &debug.Module{Path: "../summercms.go", Version: "", Sum: ""},
|
||||
},
|
||||
},
|
||||
Settings: []debug.BuildSetting{
|
||||
{Key: "GOOS", Value: "linux"},
|
||||
{Key: "vcs.revision", Value: "abc123"},
|
||||
{Key: "vcs.modified", Value: "false"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIDBuildInfo(t *testing.T) {
|
||||
root := buildFixture()
|
||||
distOnly := mustBuildID(t, root, nil)
|
||||
base := mustBuildID(t, root, buildInfoFixture())
|
||||
if base == distOnly {
|
||||
t.Fatal("build info did not change the id")
|
||||
}
|
||||
if again := mustBuildID(t, root, buildInfoFixture()); again != base {
|
||||
t.Fatalf("same info gave %q and %q", base, again)
|
||||
}
|
||||
reversed := buildInfoFixture()
|
||||
slices.Reverse(reversed.Deps)
|
||||
if id := mustBuildID(t, root, reversed); id != base {
|
||||
t.Fatalf("dependency order changed the id: %q vs %q", id, base)
|
||||
}
|
||||
|
||||
setting := func(info *debug.BuildInfo, key, value string) {
|
||||
for i := range info.Settings {
|
||||
if info.Settings[i].Key == key {
|
||||
info.Settings[i].Value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
changes := map[string]func(*debug.BuildInfo){
|
||||
"vcs.revision": func(i *debug.BuildInfo) { setting(i, "vcs.revision", "def456") },
|
||||
"vcs.modified": func(i *debug.BuildInfo) { setting(i, "vcs.modified", "true") },
|
||||
"main version": func(i *debug.BuildInfo) { i.Main.Version = "v1.2.4" },
|
||||
"main sum": func(i *debug.BuildInfo) { i.Main.Sum = "h1:other=" },
|
||||
"go version": func(i *debug.BuildInfo) { i.GoVersion = "go1.26.1" },
|
||||
"dependency version": func(i *debug.BuildInfo) { i.Deps[0].Version = "v0.4.1" },
|
||||
"dependency sum": func(i *debug.BuildInfo) { i.Deps[0].Sum = "h1:blog2=" },
|
||||
"added dependency": func(i *debug.BuildInfo) {
|
||||
i.Deps = append(i.Deps, &debug.Module{Path: "example.com/shop", Version: "v1.0.0", Sum: "h1:shop="})
|
||||
},
|
||||
"replace version": func(i *debug.BuildInfo) { i.Deps[1].Replace.Version = "v0.9.1" },
|
||||
}
|
||||
for name, change := range changes {
|
||||
info := buildInfoFixture()
|
||||
change(info)
|
||||
if id := mustBuildID(t, root, info); id == base {
|
||||
t.Fatalf("%s kept the id %q", name, id)
|
||||
}
|
||||
}
|
||||
|
||||
unrelated := buildInfoFixture()
|
||||
setting(unrelated, "GOOS", "darwin")
|
||||
unrelated.Settings = append(unrelated.Settings, debug.BuildSetting{Key: "-tags", Value: "acme"})
|
||||
if id := mustBuildID(t, root, unrelated); id != base {
|
||||
t.Fatalf("an unrelated setting changed the id: %q vs %q", id, base)
|
||||
}
|
||||
|
||||
novcs := buildInfoFixture()
|
||||
novcs.Settings = []debug.BuildSetting{{Key: "GOOS", Value: "linux"}}
|
||||
mustBuildID(t, root, novcs)
|
||||
}
|
||||
|
||||
func stubBuildInfo(t *testing.T, info *debug.BuildInfo, ok bool) {
|
||||
t.Helper()
|
||||
saved := readBuildInfo
|
||||
readBuildInfo = func() (*debug.BuildInfo, bool) { return info, ok }
|
||||
t.Cleanup(func() { readBuildInfo = saved })
|
||||
}
|
||||
|
||||
// servedBuild returns the build id the handler serves in the meta, the
|
||||
// header and build.json, failing when they disagree.
|
||||
func servedBuild(t *testing.T, h http.Handler) string {
|
||||
t.Helper()
|
||||
index := get(h, testPrefix+"/")
|
||||
meta := regexp.MustCompile(`<meta name="summer-admin-build" content="([^"]*)"`).FindStringSubmatch(index.Body.String())
|
||||
if meta == nil {
|
||||
t.Fatalf("index has no build meta: %s", index.Body.String())
|
||||
}
|
||||
rec := get(h, testPrefix+"/"+BuildPath)
|
||||
var body struct{ Build string }
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("build.json %s: %v", rec.Body.String(), err)
|
||||
}
|
||||
header := index.Header().Get(BuildHeader)
|
||||
if meta[1] != body.Build || header != body.Build {
|
||||
t.Fatalf("meta %q, build.json %q and header %q disagree", meta[1], body.Build, header)
|
||||
}
|
||||
return body.Build
|
||||
}
|
||||
|
||||
func TestBuildInfoFallback(t *testing.T) {
|
||||
root := buildFixture()
|
||||
|
||||
stubBuildInfo(t, nil, false)
|
||||
h, err := newHandler(root, testPrefix, &apiSpy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := servedBuild(t, h), mustBuildID(t, root, nil); got != want {
|
||||
t.Fatalf("without build info served %q, want the dist id %q", got, want)
|
||||
}
|
||||
|
||||
fixture := buildInfoFixture()
|
||||
stubBuildInfo(t, fixture, true)
|
||||
h, err = newHandler(root, testPrefix, &apiSpy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := servedBuild(t, h)
|
||||
if want := mustBuildID(t, root, fixture); first != want {
|
||||
t.Fatalf("with build info served %q, want %q", first, want)
|
||||
}
|
||||
|
||||
redeploy := buildInfoFixture()
|
||||
redeploy.Settings[1].Value = "def456"
|
||||
stubBuildInfo(t, redeploy, true)
|
||||
h, err = newHandler(root, testPrefix, &apiSpy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second := servedBuild(t, h); second == first {
|
||||
t.Fatalf("a new vcs.revision served the same id %q", second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHeaderOnEveryResponse(t *testing.T) {
|
||||
h, spy := newTestHandler(t)
|
||||
root, err := Dist()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := mustBuildID(t, root, buildInfo())
|
||||
entries, err := fs.ReadDir(root, "assets")
|
||||
if err != nil || len(entries) == 0 {
|
||||
t.Fatalf("embedded assets: %v", err)
|
||||
}
|
||||
for _, target := range []string{
|
||||
testPrefix + "/",
|
||||
testPrefix + "/index.html",
|
||||
testPrefix + "/acme/demo/widgets",
|
||||
testPrefix + "/assets/" + entries[0].Name(),
|
||||
testPrefix + "/missing.js",
|
||||
testPrefix + "/" + BuildPath,
|
||||
testPrefix + "/api/v1/nope",
|
||||
} {
|
||||
if got := get(h, target).Header().Get(BuildHeader); got != want {
|
||||
t.Fatalf("%s %s=%q, want %q", target, BuildHeader, got, want)
|
||||
}
|
||||
}
|
||||
if spy.calls != 1 {
|
||||
t.Fatalf("api miss reached the spy %d times", spy.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEndpoint(t *testing.T) {
|
||||
root := buildFixture()
|
||||
h, err := newHandler(root, testPrefix, &apiSpy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := get(h, testPrefix+"/"+BuildPath)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d", rec.Code)
|
||||
}
|
||||
if got := rec.Header().Get("Content-Type"); got != "application/json" {
|
||||
t.Fatalf("Content-Type=%q", got)
|
||||
}
|
||||
if got := rec.Header().Get("Cache-Control"); got != "no-store" {
|
||||
t.Fatalf("Cache-Control=%q", got)
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("body %s: %v", rec.Body.String(), err)
|
||||
}
|
||||
if len(body) != 1 || body["build"] != mustBuildID(t, root, buildInfo()) {
|
||||
t.Fatalf("body = %v", body)
|
||||
}
|
||||
|
||||
// The build route wins over a dist file of the same name.
|
||||
shadowed := buildFixture()
|
||||
shadowed[BuildPath] = &fstest.MapFile{Data: []byte(`{"build":"stale"}`)}
|
||||
h, err = newHandler(shadowed, testPrefix, &apiSpy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := get(h, testPrefix+"/"+BuildPath).Body.String(); strings.Contains(got, "stale") {
|
||||
t.Fatalf("dist build.json shadowed the route: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildToken(t *testing.T) {
|
||||
root := buildFixture()
|
||||
h, err := newHandler(root, testPrefix, &apiSpy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := get(h, testPrefix+"/").Body.String()
|
||||
if strings.Contains(body, BuildToken) {
|
||||
t.Fatalf("token left in the index: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `content="`+mustBuildID(t, root, buildInfo())+`"`) {
|
||||
t.Fatalf("meta not replaced: %s", body)
|
||||
}
|
||||
|
||||
raw := []byte(`<html><meta name="summer-admin-base" content="` + BaseToken + `" /><script src="./assets/app.js"></script></html>`)
|
||||
plain := fstest.MapFS{
|
||||
"index.html": &fstest.MapFile{Data: raw},
|
||||
"assets/app.js": &fstest.MapFile{Data: []byte(`1`)},
|
||||
}
|
||||
h, err = newHandler(plain, testPrefix, &apiSpy{})
|
||||
if err != nil {
|
||||
t.Fatalf("index without the build token: %v", err)
|
||||
}
|
||||
want, err := RewriteIndex(raw, testPrefix)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := get(h, testPrefix+"/").Body.String(); got != string(want) {
|
||||
t.Fatalf("index without the token changed:\n%s\nwant\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedBuild(t *testing.T) {
|
||||
h, _ := newTestHandler(t)
|
||||
root, err := Dist()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := servedBuild(t, h), mustBuildID(t, root, buildInfo()); got != want {
|
||||
t.Fatalf("embedded build served %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user