diff --git a/modules/mesh-build-node/cmd/mesh-build-node/publish_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/publish_integration_test.go new file mode 100644 index 00000000..50882d7e --- /dev/null +++ b/modules/mesh-build-node/cmd/mesh-build-node/publish_integration_test.go @@ -0,0 +1,121 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + + "git.novox.be/novox/mesh-sdk/go/forgekey" +) + +// npmRegistry is a registry of the test's own speaking npm's publish protocol: a PUT of the package's document with the +// packed file attached, then GET of the document and the file; it asks for the account's credential. +type npmRegistry struct { + mu sync.Mutex + versions map[string]map[string]any + files map[string][]byte + publishes int +} + +func (r *npmRegistry) serve(t *testing.T) *httptest.Server { + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, q *http.Request) { + r.mu.Lock() + defer r.mu.Unlock() + if u, p, ok := q.BasicAuth(); !ok || u != "builder" || p != "the write credential" { + w.WriteHeader(http.StatusUnauthorized) + return + } + doc := strings.TrimPrefix(q.URL.EscapedPath(), "/npm/") + switch { + case q.Method == http.MethodPut && (doc == "@novox%2fmesh-sdk" || doc == "@novox%2Fmesh-sdk"): + var body struct { + Versions map[string]map[string]any `json:"versions"` + Attachments map[string]struct { + Data string `json:"data"` + } `json:"_attachments"` + } + raw, _ := io.ReadAll(q.Body) + if err := json.Unmarshal(raw, &body); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + for v, meta := range body.Versions { + for name, a := range body.Attachments { + data, _ := base64.StdEncoding.DecodeString(a.Data) + r.files[name] = data + meta["dist"] = map[string]any{"tarball": srv.URL + "/files/" + name, "integrity": integrityOf(data)} + } + r.versions[v] = meta + } + r.publishes++ + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"ok":true}`)) + case q.Method == http.MethodGet && (doc == "@novox%2fmesh-sdk" || doc == "@novox%2Fmesh-sdk"): + if len(r.versions) == 0 { + http.NotFound(w, q) + return + } + json.NewEncoder(w).Encode(map[string]any{"name": "@novox/mesh-sdk", "versions": r.versions}) + case strings.HasPrefix(q.URL.Path, "/files/"): + w.Write(r.files[strings.TrimPrefix(q.URL.Path, "/files/")]) + default: + http.NotFound(w, q) + } + })) + t.Cleanup(srv.Close) + return srv +} + +// 1587-S20 (ADR 0498 ยง5): the real npm publishes a trunk build's package to a registry speaking npm's protocol, with +// the module's credential, once: a second trunk build of the same package finds its file there and publishes nothing; +// a candidate publishes nothing at all. +func TestARealTrunkPackageIsPublishedOnce(t *testing.T) { + for _, tool := range []string{"node", "npm", "bwrap"} { + if _, err := exec.LookPath(tool); err != nil { + t.Skipf("NOT RUN: no %s where this check runs; every build node has it", tool) + } + } + m, _, work := newMachine(t) + m.Run = forgekey.ExecRunner + reg := &npmRegistry{versions: map[string]map[string]any{}, files: map[string][]byte{}} + srv := reg.serve(t) + m.Toolchain.Registry = func() (Registry, error) { + return Registry{URL: srv.URL + "/npm/", Username: "builder", Password: "the write credential"}, nil + } + m.HTTP = srv.Client() + files := map[string]string{} + for n, c := range sdkFixture { + files[n] = c + } + workspaceWith(t, work, files) + dir := filepath.Join(work, "w1", "tree") + if err := m.installDev(context.Background(), filepath.Join(work, "w1"), dir, "."); err != nil { + t.Fatal(err) + } + args := map[string]any{"workspace": "w1", "module": ".", "artifact": "lib"} + if out, err := m.build(context.Background(), args); err != nil || out.(BuildResult).Reference != "" || reg.publishes != 0 { + t.Fatalf("a candidate: %+v %v, %d publishes", out, err, reg.publishes) + } + args["publish"] = true + out, err := m.build(context.Background(), args) + if err != nil || out.(BuildResult).Reference != "@novox/mesh-sdk@9.9.9" || reg.publishes != 1 { + t.Fatalf("a trunk build: %+v %v, %d publishes", out, err, reg.publishes) + } + if got := digestOf(reg.files["@novox/mesh-sdk-9.9.9.tgz"]); got != out.(BuildResult).Digest { + t.Errorf("the registry holds %s, the build packed %s (files %d)", got, out.(BuildResult).Digest, len(reg.files)) + } + if again, err := m.build(context.Background(), args); err != nil || again.(BuildResult).Reference != "@novox/mesh-sdk@9.9.9" || reg.publishes != 1 { + t.Errorf("the same package again: %+v %v, %d publishes", again, err, reg.publishes) + } +}