176 lines
6.3 KiB
Go
176 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
type npmCall struct {
|
|
name string
|
|
args []string
|
|
env []string
|
|
}
|
|
|
|
type fakeNpm struct {
|
|
mu sync.Mutex
|
|
calls []npmCall
|
|
ran forgekey.Ran
|
|
}
|
|
|
|
func (f *fakeNpm) run(_ context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, npmCall{name, args, env})
|
|
if f.ran.Status == 0 && f.ran.Err == "" {
|
|
// npm ci writes node_modules into --prefix.
|
|
for i, a := range args {
|
|
if a == "--prefix" {
|
|
os.MkdirAll(filepath.Join(args[i+1], "node_modules"), 0o700)
|
|
}
|
|
}
|
|
}
|
|
return f.ran
|
|
}
|
|
|
|
func toolchainIn(t *testing.T, fake *fakeNpm) Toolchain {
|
|
t.Helper()
|
|
home, cache := t.TempDir(), t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(home, "toolchain"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.WriteFile(filepath.Join(home, "toolchain", "package.json"), []byte(`{"name":"x"}`), 0o600)
|
|
os.WriteFile(filepath.Join(home, "toolchain", "package-lock.json"), []byte(`{"lock":1}`), 0o600)
|
|
return Toolchain{Paths: Paths{Home: home, Cache: cache}, Run: fake.run,
|
|
Registry: func() (Registry, error) {
|
|
return Registry{URL: "https://git.novox.be/api/packages/novox/npm/", Username: "mesh-build-node", Password: "pw"}, nil
|
|
}}
|
|
}
|
|
|
|
// 1488/AC4: given a missing toolchain directory, npm ci --ignore-scripts runs once in <cache>/toolchain/<recipe
|
|
// version> with the module's lock; asked again with the directory present, nothing runs.
|
|
func TestAMissingToolchainDirectoryIsInstalledOnceFromTheLock(t *testing.T) {
|
|
fake := &fakeNpm{}
|
|
tc := toolchainIn(t, fake)
|
|
version := Recipe{Steps: "1", Lock: "x"}.Version()
|
|
dir := filepath.Join(tc.Paths.Cache, "toolchain", version)
|
|
if tc.Present(version) {
|
|
t.Fatal("present before any install")
|
|
}
|
|
if err := tc.Ensure(context.Background(), version); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(fake.calls) != 1 {
|
|
t.Fatalf("npm ran %d times, want once", len(fake.calls))
|
|
}
|
|
c := fake.calls[0]
|
|
joined := strings.Join(c.args, " ")
|
|
if c.name != "npm" || c.args[0] != "ci" || !strings.Contains(joined, "--ignore-scripts") || !strings.Contains(joined, "--no-bin-links") || !strings.Contains(joined, "--prefix "+dir) {
|
|
t.Errorf("ran %s %v, want npm ci --ignore-scripts --prefix %s", c.name, c.args, dir)
|
|
}
|
|
if lock, _ := os.ReadFile(filepath.Join(dir, "package-lock.json")); string(lock) != `{"lock":1}` {
|
|
t.Errorf("the directory holds lock %q, want the module's", lock)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, ".npmrc-install")); err == nil {
|
|
t.Errorf("the registry credential file was left in the toolchain directory")
|
|
}
|
|
if !tc.Present(version) {
|
|
t.Errorf("not present after the install")
|
|
}
|
|
if err := tc.Ensure(context.Background(), version); err != nil || len(fake.calls) != 1 {
|
|
t.Errorf("a present directory ran npm again: %d calls, %v", len(fake.calls), err)
|
|
}
|
|
}
|
|
|
|
// 1488/AC4: the install is told the mesh's registry for @novox with the account's credential, and only through a
|
|
// config file of its own, never on the command line.
|
|
func TestTheInstallNamesTheRegistryInAConfigFileNotOnTheCommandLine(t *testing.T) {
|
|
var npmrc string
|
|
fake := &fakeNpm{}
|
|
tc := toolchainIn(t, fake)
|
|
tc.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
|
|
for i, a := range args {
|
|
if a == "--userconfig" {
|
|
raw, _ := os.ReadFile(args[i+1])
|
|
npmrc = string(raw)
|
|
}
|
|
}
|
|
return fake.run(ctx, env, name, args...)
|
|
}
|
|
if err := tc.Ensure(context.Background(), "v1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(npmrc, "@novox:registry=https://git.novox.be/api/packages/novox/npm/\n") ||
|
|
!strings.Contains(npmrc, "//git.novox.be/api/packages/novox/npm/:_auth=") {
|
|
t.Errorf("npmrc = %q", npmrc)
|
|
}
|
|
if strings.Contains(strings.Join(fake.calls[0].args, " "), "pw") {
|
|
t.Errorf("the credential is on the command line: %v", fake.calls[0].args)
|
|
}
|
|
}
|
|
|
|
// 1488/AC4: an install that fails leaves no directory taken for a toolchain, says why, and the next call tries again.
|
|
func TestAFailedInstallIsNotAToolchain(t *testing.T) {
|
|
fake := &fakeNpm{ran: forgekey.Ran{Status: 1, Stderr: "E401 unauthorized"}}
|
|
tc := toolchainIn(t, fake)
|
|
err := tc.Ensure(context.Background(), "v1")
|
|
if err == nil || !strings.Contains(err.Error(), "E401") {
|
|
t.Fatalf("err = %v, want the npm failure named", err)
|
|
}
|
|
if tc.Present("v1") {
|
|
t.Errorf("a failed install is present")
|
|
}
|
|
fake.ran = forgekey.Ran{}
|
|
if err := tc.Ensure(context.Background(), "v1"); err != nil || len(fake.calls) != 2 || !tc.Present("v1") {
|
|
t.Errorf("the retry: %v, %d calls", err, len(fake.calls))
|
|
}
|
|
}
|
|
|
|
// 1488/AC4: a module's steps run the install behind the guard: a refusing node installs nothing, an accepting one
|
|
// installs once however many steps follow.
|
|
func TestPrepareInstallsOnlyBehindAPassingGuard(t *testing.T) {
|
|
fake := &fakeNpm{}
|
|
tc := toolchainIn(t, fake)
|
|
states := newStates()
|
|
m := moduleWith(states, "")
|
|
m.Probe = func(context.Context) (string, error) { return "", errors.New("no node") }
|
|
m.Toolchain = &tc
|
|
if err := m.Prepare(context.Background()); err == nil || len(fake.calls) != 0 {
|
|
t.Fatalf("a refusing node: %v, %d npm calls", err, len(fake.calls))
|
|
}
|
|
m = moduleWith(states, "v26.10.0")
|
|
m.Toolchain = &tc
|
|
for i := 0; i < 3; i++ {
|
|
if err := m.Prepare(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if len(fake.calls) != 1 {
|
|
t.Errorf("npm ran %d times for three steps, want once", len(fake.calls))
|
|
}
|
|
}
|
|
|
|
func TestRegistryFromTheBinding(t *testing.T) {
|
|
dir := t.TempDir()
|
|
b, s := filepath.Join(dir, "b.json"), filepath.Join(dir, "s")
|
|
os.WriteFile(s, []byte("secret\n"), 0o600)
|
|
os.WriteFile(b, []byte(`{"provision":"npm-package-registry","at":"10.42.0.1","as":"mesh-build-node-ace","serves":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/"}}`), 0o600)
|
|
r, err := ReadRegistry(b, s)
|
|
if err != nil || r.URL != "http://10.42.0.1:3000/api/packages/novox/npm/" || r.Username != "mesh-build-node-ace" || r.Password != "secret" {
|
|
t.Fatalf("= %+v, %v", r, err)
|
|
}
|
|
os.WriteFile(b, []byte(`{"at":"h","as":"a","serves":{"port":1,"npm-path":"/p/","public-root":"https://git.novox.be/p"}}`), 0o600)
|
|
if r, _ := ReadRegistry(b, s); r.URL != "https://git.novox.be/p/" {
|
|
t.Errorf("the public root is not preferred: %q", r.URL)
|
|
}
|
|
if _, err := ReadRegistry(filepath.Join(dir, "none"), s); err == nil {
|
|
t.Errorf("no binding was accepted")
|
|
}
|
|
}
|