Files
mesh-catalog/modules/mesh-build-node/cmd/mesh-build-node/package_test.go
T
jschoubben b9cdca78b0 mesh-build-node: narrow the fetch to the named paths, build members against the SDK their lock names, and build, pack and publish a package (issues 1758, 1587 S15-S19; ADR 0499 §4, ADR 0498 §5, ADR 0500 §4-§6; lead decision L10)
build_node_fetch takes buildstep.FetchRequest (dir and paths narrow the workspace) and an
optional package {repository, commit, ref}: the group's package member, built and packed
shut off and published nowhere. Each member's lock decides its SDK: the group's packed
file when the version and integrity are the lock's, else the registry's (integrity
checked); a version the registry lacks fails waits for: @novox/mesh-sdk@<version>. A
package artifact is built by its own script and packed with npm pack, shut off; only a
trunk build publishes it, from this process with the write credential (never in a
workspace): absent is published, the same file is done, another is version taken:. The
registry configuration file now lies outside every workspace.
2026-10-22 18:38:22 +02:00

161 lines
7.1 KiB
Go

package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// tgzOf is a packed npm package holding package/package.json of name@version and one more file.
func tgzOf(t *testing.T, name, version string) []byte {
t.Helper()
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
tw := tar.NewWriter(zw)
for _, f := range [][2]string{{"package/package.json", `{"name":"` + name + `","version":"` + version + `"}`}, {"package/dist/index.js", "export const v = '" + version + "';\n"}} {
n, body := f[0], f[1]
tw.WriteHeader(&tar.Header{Name: n, Mode: 0o644, Size: int64(len(body)), Typeflag: tar.TypeReg})
tw.Write([]byte(body))
}
tw.Close()
zw.Close()
return buf.Bytes()
}
// fakeRegistry serves the packument and the packed files of the versions it holds; asked counts its answers.
func fakeRegistry(t *testing.T, m *Module, held map[string][]byte) *atomic.Int32 {
t.Helper()
var asked atomic.Int32
var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
asked.Add(1)
switch {
case r.URL.Path == "/npm/@novox%2fmesh-sdk" || r.URL.Path == "/npm/@novox/mesh-sdk":
if len(held) == 0 {
http.NotFound(w, r)
return
}
versions := map[string]any{}
for v, tgz := range held {
versions[v] = map[string]any{"dist": map[string]any{"tarball": srv.URL + "/files/" + v + ".tgz", "integrity": integrityOf(tgz)}}
}
json.NewEncoder(w).Encode(map[string]any{"versions": versions})
case strings.HasPrefix(r.URL.Path, "/files/"):
v := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/files/"), ".tgz")
if tgz, ok := held[v]; ok {
w.Write(tgz)
return
}
http.NotFound(w, r)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
m.Toolchain.Registry = func() (Registry, error) { return Registry{URL: srv.URL + "/npm/"}, nil }
m.HTTP = srv.Client()
return &asked
}
// memberLocking is a member's directory whose lock names @novox/mesh-sdk@version with integrity (none when version is "").
func memberLocking(t *testing.T, version, integrity string) string {
t.Helper()
dir := t.TempDir()
lock := map[string]any{"lockfileVersion": 3, "packages": map[string]any{"": map[string]any{"name": "m"}}}
if version != "" {
lock["packages"].(map[string]any)["node_modules/@novox/mesh-sdk"] = map[string]any{"version": version, "integrity": integrity}
}
raw, _ := json.Marshal(lock)
write(t, filepath.Join(dir, "package-lock.json"), string(raw), 0o644)
return dir
}
func placedVersion(dir string) string {
raw, err := os.ReadFile(filepath.Join(dir, "node_modules", "@novox", "mesh-sdk", "package.json"))
if err != nil {
return ""
}
var p packageJSON
json.Unmarshal(raw, &p)
return p.Version
}
// The lead's decision L10 (ADR 0500 §6): a member is built against the SDK version its own lock names, downloaded from
// the registry and checked against the lock's integrity; a version the registry does not hold is a wait, `waits for:`.
func TestAMemberGetsTheSDKVersionItsLockNamesOrWaitsForIt(t *testing.T) {
m, _, _ := newMachine(t)
held := tgzOf(t, sdkPackage, "0.1.15")
fakeRegistry(t, m, map[string][]byte{"0.1.15": held})
dir := memberLocking(t, "0.1.15", integrityOf(held))
if err := m.placeSDK(context.Background(), dir, "modules/m", nil); err != nil || placedVersion(dir) != "0.1.15" {
t.Errorf("the registry's 0.1.15: %v, placed %q", err, placedVersion(dir))
}
dir = memberLocking(t, "0.1.16", "sha512-x")
err := m.placeSDK(context.Background(), dir, "modules/m", nil)
if err == nil || err.Error() != delivery.WaitsFor+"@novox/mesh-sdk@0.1.16" {
t.Errorf("a version the registry lacks answered %v, want %s@novox/mesh-sdk@0.1.16", err, delivery.WaitsFor)
}
dir = memberLocking(t, "0.1.15", "sha512-another")
if err := m.placeSDK(context.Background(), dir, "modules/m", nil); err == nil || !strings.Contains(err.Error(), "sha512-another") {
t.Errorf("a registry file of another integrity than the lock's answered %v", err)
}
}
// ADR 0500 §5 (S3, S4): the group's packed package is served in place of the registry's when the member's lock names its
// version with its integrity; another integrity, or a version neither the group nor the registry holds, fails naming both.
// A member whose lock names no SDK keeps the toolchain's and asks the registry nothing.
func TestTheGroupsPackageIsServedToAMemberLockingItsVersion(t *testing.T) {
m, _, _ := newMachine(t)
asked := fakeRegistry(t, m, map[string][]byte{"0.1.14": tgzOf(t, sdkPackage, "0.1.14")})
tgz := tgzOf(t, sdkPackage, "0.1.15")
group := &packed{Name: sdkPackage, Version: "0.1.15", Integrity: integrityOf(tgz), Digest: digestOf(tgz), tgz: tgz}
dir := memberLocking(t, "0.1.15", group.Integrity)
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err != nil || placedVersion(dir) != "0.1.15" || asked.Load() != 0 {
t.Errorf("the group's own version: %v, placed %q, the registry asked %d times", err, placedVersion(dir), asked.Load())
}
dir = memberLocking(t, "0.1.15", "sha512-other")
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err == nil || !strings.Contains(err.Error(), "sha512-other") || !strings.Contains(err.Error(), group.Integrity) {
t.Errorf("another integrity answered %v, want both named", err)
}
dir = memberLocking(t, "0.1.14", integrityOf(tgzOf(t, sdkPackage, "0.1.14")))
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err != nil || placedVersion(dir) != "0.1.14" {
t.Errorf("a member locking the registry's 0.1.14 in a group: %v, placed %q", err, placedVersion(dir))
}
dir = memberLocking(t, "0.1.13", "sha512-x")
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err == nil || !strings.Contains(err.Error(), "0.1.13") || !strings.Contains(err.Error(), "0.1.15") || strings.HasPrefix(err.Error(), delivery.WaitsFor) {
t.Errorf("a version neither holds answered %v, want both named and no wait", err)
}
before := asked.Load()
dir = memberLocking(t, "", "")
if err := m.placeSDK(context.Background(), dir, "modules/m", group); err != nil || placedVersion(dir) != "" || asked.Load() != before {
t.Errorf("a lock naming no SDK: %v, placed %q, the registry asked", err, placedVersion(dir))
}
}
// The fetch's `package` argument is a repository and a full commit, with a pull request's head at most; anything else is
// refused before anything is fetched.
func TestTheGroupPackageArgumentIsRefusedWhenItIsNotOne(t *testing.T) {
for _, bad := range []any{"novox/mesh-sdk", map[string]any{"repository": "novox/mesh-sdk", "commit": "main"},
map[string]any{"repository": "../x", "commit": strings.Repeat("a", 40)},
map[string]any{"repository": "novox/mesh-sdk", "commit": strings.Repeat("a", 40), "ref": "refs/heads/main"}} {
if _, err := groupPackageArg(bad); err == nil {
t.Errorf("%v was taken", bad)
}
}
g, err := groupPackageArg(map[string]any{"repository": "novox/mesh-sdk", "commit": strings.Repeat("a", 40), "ref": "refs/pull/9/head"})
if err != nil || g.Ref != "refs/pull/9/head" {
t.Errorf("a pull request's head: %+v %v", g, err)
}
}