diff --git a/src/installer/cache_integrity.go b/src/installer/cache_integrity.go index 69bfbd2..9037d9d 100644 --- a/src/installer/cache_integrity.go +++ b/src/installer/cache_integrity.go @@ -77,6 +77,12 @@ func verifyCachedNodeArchiveIntegrity(ctx context.Context, version, archivePath return finishDownloadCacheVerification(version, archivePath) } + // local_install_only: never fetch SHASUMS256.txt. Trust the staged + // archive the same way machine-policy LocalInstallDir does. + if cfg.LocalOnly { + return finishDownloadCacheVerification(version, archivePath) + } + if err := verifyArchiveWithMirrorSHASUMFn(ctx, version, archivePath, cfg); err != nil { return err } diff --git a/src/installer/cache_integrity_test.go b/src/installer/cache_integrity_test.go index 3968257..fe181c1 100644 --- a/src/installer/cache_integrity_test.go +++ b/src/installer/cache_integrity_test.go @@ -177,6 +177,49 @@ func TestVerifyCachedNodeArchiveIntegrityTrustedLocalInstallBypass(t *testing.T) } } +func TestVerifyCachedNodeArchiveIntegrityLocalOnlySkipsMirror(t *testing.T) { + dir := t.TempDir() + archivePath := filepath.Join(dir, "node-v22.0.0-win-x64.7z") + if err := os.WriteFile(archivePath, []byte("airgapped local archive"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + origVerify := verifyDownloadArchiveCacheFn + verifyDownloadArchiveCacheFn = func(string) error { return verifycache.ErrDownloadCacheMiss } + t.Cleanup(func() { verifyDownloadArchiveCacheFn = origVerify }) + + origTrusted := isMachinePolicyTrustedLocalArchiveFn + isMachinePolicyTrustedLocalArchiveFn = func(string) (bool, error) { return false, nil } + t.Cleanup(func() { isMachinePolicyTrustedLocalArchiveFn = origTrusted }) + + mirrorCalled := false + origMirror := verifyArchiveWithMirrorSHASUMFn + verifyArchiveWithMirrorSHASUMFn = func(context.Context, string, string, InstallConfig) error { + mirrorCalled = true + return errors.New("mirror should not be called") + } + t.Cleanup(func() { verifyArchiveWithMirrorSHASUMFn = origMirror }) + + signed := false + origSign := signDownloadArchiveCacheFn + signDownloadArchiveCacheFn = func(string) error { + signed = true + return nil + } + t.Cleanup(func() { signDownloadArchiveCacheFn = origSign }) + + cfg := InstallConfig{LocalOnly: true} + if err := verifyCachedNodeArchiveIntegrity(t.Context(), "22.0.0", archivePath, cfg); err != nil { + t.Fatalf("verifyCachedNodeArchiveIntegrity() error = %v", err) + } + if mirrorCalled { + t.Fatal("mirror SHASUM verification was called, want local-only skip") + } + if !signed { + t.Fatal("expected SignDownloadArchiveCache after local-only trust") + } +} + func TestVerifyCachedNodeArchiveIntegrityFallsBackToMirrorSHASUM(t *testing.T) { dir := t.TempDir() archiveName := "node-v22.0.0-win-x64.7z" diff --git a/src/test/commands/cli_t3_test.go b/src/test/commands/cli_t3_test.go index e075019..d31fda6 100644 --- a/src/test/commands/cli_t3_test.go +++ b/src/test/commands/cli_t3_test.go @@ -1,9 +1,12 @@ package commands_test import ( + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" + "sync/atomic" "testing" "nvm/test/clitest" @@ -57,15 +60,39 @@ func TestCLIInstallSkipsAlreadyInstalled(t *testing.T) { func TestCLIInstallLocalOnlyMissingArchive(t *testing.T) { sb := clitest.NewSandbox(t) - sb.ApplyNodeMirrorFixture(t, clitest.TestdataPath(t, "index.tab")) if err := sb.WriteSetting("local_install_only", true); err != nil { t.Fatalf("WriteSetting(local_install_only) error = %v", err) } + prevFixtures := os.Getenv("NVM_TEST_HTTP_FIXTURES") + if err := os.Setenv("NVM_TEST_HTTP_FIXTURES", "1"); err != nil { + t.Fatalf("Setenv(NVM_TEST_HTTP_FIXTURES) error = %v", err) + } + t.Cleanup(func() { + if prevFixtures == "" { + _ = os.Unsetenv("NVM_TEST_HTTP_FIXTURES") + return + } + _ = os.Setenv("NVM_TEST_HTTP_FIXTURES", prevFixtures) + }) + + var hits atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + hits.Add(1) + http.Error(w, "catalog should not be fetched", http.StatusServiceUnavailable) + })) + t.Cleanup(srv.Close) + if err := sb.WriteSetting("node_mirror", srv.URL); err != nil { + t.Fatalf("WriteSetting(node_mirror) error = %v", err) + } + stdout, stderr, err := sb.ExecuteWithSyncStub("install", "22.0.0") if err == nil { t.Fatal("Execute(install 22.0.0) expected error, got nil") } + if n := hits.Load(); n != 0 { + t.Fatalf("catalog fetched %d time(s), want 0 for exact local-only install", n) + } combined := stdout + stderr + err.Error() if !strings.Contains(combined, "local install directory") { t.Fatalf("Execute(install 22.0.0) output = stdout:%q stderr:%q err:%v, want local install error", stdout, stderr, err)