mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
`pmg setup doctor` reported npm protection as OK on a machine with no npm installed. Two compounding defects: 1. The availability gate used a plain `exec.LookPath`, which resolves the PMG shim on PATH rather than the real binary, so the "skip" branch never fired. It now uses `shim.ResolveRealBinary` (PATH with shim dirs stripped), matching the runner, so a missing real binary correctly yields WARN "not available". 2. The result was inferred purely from a non-zero exit, so PackageManagerNotFound (exit 127) and other failures were misread as a successful block. The check now requires PMG's block headline in the captured output before reporting PASS; other non-zero exits report WARN with the error surfaced. The block headline is extracted into `ui.MalwareBlockedHeadline` so the doctor's marker stays in sync with what PMG prints across its block-output sites.
146 lines
3.9 KiB
Go
146 lines
3.9 KiB
Go
package doctor
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/safedep/pmg/internal/ui"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestEvaluateProtectionResult(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pm string
|
|
pkg string
|
|
output string
|
|
err error
|
|
wantStatus CheckStatus
|
|
}{
|
|
{
|
|
name: "blocked with headline in output",
|
|
pm: "npm",
|
|
pkg: "safedep-test-pkg@0.1.3",
|
|
output: "✗ " + ui.MalwareBlockedHeadline + "\n safedep-test-pkg@0.1.3\n",
|
|
err: fmt.Errorf("exit status 1"),
|
|
wantStatus: StatusPass,
|
|
},
|
|
{
|
|
name: "not blocked",
|
|
pm: "npm",
|
|
pkg: "safedep-test-pkg@0.1.3",
|
|
output: "",
|
|
err: nil,
|
|
wantStatus: StatusFail,
|
|
},
|
|
{
|
|
name: "pmg binary not found",
|
|
pm: "npm",
|
|
pkg: "safedep-test-pkg@0.1.3",
|
|
err: &exec.Error{Name: "pmg", Err: exec.ErrNotFound},
|
|
wantStatus: StatusWarn,
|
|
},
|
|
{
|
|
name: "non-zero exit without block headline is inconclusive",
|
|
pm: "npm",
|
|
pkg: "safedep-test-pkg@0.1.3",
|
|
output: "npm is not installed\n",
|
|
err: fmt.Errorf("exit status 127"),
|
|
wantStatus: StatusWarn,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := evaluateProtectionResult(tt.pm, tt.pkg, tt.output, tt.err)
|
|
assert.Equal(t, tt.wantStatus, result.Status)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunProtectionCheckSkipsWhenOnlyShimOnPath(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
// ~/.pmg/bin suffix is stripped by FilterPMGFromPath, so a shim here must
|
|
// not be mistaken for a real npm binary.
|
|
shimDir := filepath.Join(tmp, ".pmg", "bin")
|
|
require.NoError(t, os.MkdirAll(shimDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(shimDir, "npm"), []byte("#!/bin/sh\n"), 0o755))
|
|
|
|
t.Setenv("PATH", shimDir)
|
|
|
|
tc := ProtectionTestCase{
|
|
PackageManager: "npm",
|
|
Package: "safedep-test-pkg@0.1.3",
|
|
InstallArgs: []string{"npm", "install", "safedep-test-pkg@0.1.3"},
|
|
}
|
|
|
|
result := RunProtectionCheck(tc, filepath.Join(tmp, "nonexistent-pmg"))
|
|
assert.Equal(t, StatusWarn, result.Status)
|
|
assert.Contains(t, result.Message, "not available")
|
|
}
|
|
|
|
func TestProtectionTestCases(t *testing.T) {
|
|
cases := ProtectionTestCases()
|
|
require.GreaterOrEqual(t, len(cases), 2)
|
|
|
|
hasNpm := false
|
|
hasPip := false
|
|
for _, tc := range cases {
|
|
if tc.PackageManager == "npm" {
|
|
hasNpm = true
|
|
assert.False(t, tc.NeedsVenv)
|
|
assert.Contains(t, tc.InstallArgs, "--no-cache")
|
|
assert.Contains(t, tc.InstallArgs, "--prefer-online")
|
|
}
|
|
if tc.PackageManager == "pip" {
|
|
hasPip = true
|
|
assert.True(t, tc.NeedsVenv)
|
|
assert.Contains(t, tc.InstallArgs, "--no-cache-dir")
|
|
}
|
|
}
|
|
assert.True(t, hasNpm)
|
|
assert.True(t, hasPip)
|
|
}
|
|
|
|
func TestPrependPath(t *testing.T) {
|
|
env := []string{"HOME=/home/user", "PATH=/usr/bin:/bin", "TERM=xterm"}
|
|
result := prependPath(env, "/tmp/venv/bin")
|
|
|
|
require.Len(t, result, 3)
|
|
assert.Equal(t, "HOME=/home/user", result[0])
|
|
assert.True(t, strings.HasPrefix(result[1], "PATH=/tmp/venv/bin"))
|
|
assert.Contains(t, result[1], "/usr/bin:/bin")
|
|
assert.Equal(t, "TERM=xterm", result[2])
|
|
}
|
|
|
|
func TestSetupVenv(t *testing.T) {
|
|
if _, err := exec.LookPath("python3"); err != nil {
|
|
t.Skip("python3 not available")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
venvDir, err := setupVenv(tmpDir)
|
|
require.NoError(t, err)
|
|
|
|
pipPath := filepath.Join(venvDir, "bin", "pip")
|
|
_, err = os.Stat(pipPath)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCheckShimScripts(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
shimDir := filepath.Join(tmpDir, ".pmg", "bin")
|
|
require.NoError(t, os.MkdirAll(shimDir, 0o755))
|
|
|
|
shimPath := filepath.Join(shimDir, "npm")
|
|
require.NoError(t, os.WriteFile(shimPath, []byte("#!/bin/sh\nexec pmg npm \"$@\""), 0o755))
|
|
|
|
found, missing := CheckShimScripts(shimDir, []string{"npm", "pip"})
|
|
assert.Equal(t, []string{"npm"}, found)
|
|
assert.Equal(t, []string{"pip"}, missing)
|
|
}
|