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 /toolchain/ 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") } }