import { test, describe, after } from 'node:test'; import assert from 'node:assert/strict'; import { existsSync, readFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const SCRIPT = join(ROOT, 'scripts', 'setup-npm-registry.mjs'); const created = []; after(() => { while (created.length) { try { rmSync(created.pop(), { recursive: true, force: true }); } catch {} } }); function tempNpmrcPath() { const dir = mkdtempSync(join(tmpdir(), 'lh-npmrc-')); created.push(dir); return join(dir, '.npmrc'); } // Never hits the network or `gh`: --token supplies the token directly and // --skip-verify skips the GitHub API round-trip, so these tests are hermetic. function setup(args, env = {}) { try { const stdout = execFileSync(process.execPath, [SCRIPT, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...env }, }); return { code: 0, stdout, stderr: '' }; } catch (e) { return { code: e.status ?? 1, stdout: e.stdout ?? '', stderr: e.stderr ?? '' }; } } describe('setup-npm-registry script', () => { test('--dry-run writes nothing', () => { const npmrc = tempNpmrcPath(); const r = setup(['--dry-run', '--token=fake-token', '--skip-verify', `--npmrc=${npmrc}`]); assert.equal(r.code, 0); assert.ok(!existsSync(npmrc), 'dry run must not create the file'); assert.match(r.stdout, /would write/); }); test('writes scope + auth token entries, preserving unrelated lines', () => { const npmrc = tempNpmrcPath(); writeFileSync(npmrc, 'registry=https://registry.npmjs.org/\n//existing-line=keep-me\n'); const r = setup(['--token=fake-token', '--skip-verify', `--npmrc=${npmrc}`]); assert.equal(r.code, 0); const content = readFileSync(npmrc, 'utf8'); assert.match(content, /registry=https:\/\/registry\.npmjs\.org\//); assert.match(content, /existing-line=keep-me/); assert.match(content, /@redsentech:registry=https:\/\/npm\.pkg\.github\.com/); assert.match(content, /\/\/npm\.pkg\.github\.com\/:_authToken=fake-token/); }); test('never prints the token in full', () => { const npmrc = tempNpmrcPath(); const r = setup(['--token=super-secret-token-value', '--skip-verify', `--npmrc=${npmrc}`]); assert.equal(r.code, 0); assert.ok(!r.stdout.includes('super-secret-token-value'), 'full token must never be printed'); }); test('rerun is idempotent (no duplicate entries)', () => { const npmrc = tempNpmrcPath(); setup(['--token=fake-token', '--skip-verify', `--npmrc=${npmrc}`]); setup(['--token=fake-token', '--skip-verify', `--npmrc=${npmrc}`]); const content = readFileSync(npmrc, 'utf8'); const matches = content.match(/@redsentech:registry=/g) || []; assert.equal(matches.length, 1, 'entries must not be duplicated across reruns'); }); test('--unset removes only this scope/registry, keeps everything else', () => { const npmrc = tempNpmrcPath(); writeFileSync(npmrc, 'registry=https://registry.npmjs.org/\n'); setup(['--token=fake-token', '--skip-verify', `--npmrc=${npmrc}`]); const r = setup(['--unset', `--npmrc=${npmrc}`]); assert.equal(r.code, 0); const content = readFileSync(npmrc, 'utf8'); assert.match(content, /registry=https:\/\/registry\.npmjs\.org\//); assert.ok(!content.includes('@redsentech:registry='), 'scope entry must be removed'); assert.ok(!content.includes('_authToken='), 'auth token entry must be removed'); }); test('custom --scope and --registry are honoured', () => { const npmrc = tempNpmrcPath(); const r = setup(['--token=fake-token', '--skip-verify', `--npmrc=${npmrc}`, '--scope=@other', '--registry=https://example.test']); assert.equal(r.code, 0); const content = readFileSync(npmrc, 'utf8'); assert.match(content, /@other:registry=https:\/\/example\.test/); assert.match(content, /\/\/example\.test\/:_authToken=fake-token/); }); test('--no-open prints the classic-PAT URL and accepts a pasted token via stdin', () => { const npmrc = tempNpmrcPath(); const r = execFileSync(process.execPath, [ SCRIPT, '--no-open', '--skip-verify', `--npmrc=${npmrc}`, ], { encoding: 'utf8', input: 'ghp_pastedtoken1234\n', env: { ...process.env, NPM_REGISTRY_TOKEN: '', GITHUB_TOKEN: '', GH_TOKEN: '', PATH: '/nonexistent', // hide `gh` so gh-CLI discovery can't short-circuit the prompt }, }); assert.match(r, /github\.com\/settings\/tokens\/new\?scopes=read%3Apackages/); assert.match(r, /--no-open.*skipping automatic browser launch/); const content = readFileSync(npmrc, 'utf8'); assert.match(content, /_authToken=ghp_pastedtoken1234/); }); test('fails cleanly with --yes and no token available anywhere', () => { const npmrc = tempNpmrcPath(); const r = setup(['--yes', `--npmrc=${npmrc}`], { NPM_REGISTRY_TOKEN: '', GITHUB_TOKEN: '', GH_TOKEN: '', PATH: '/nonexistent', // hide `gh` from PATH so gh-CLI discovery can't accidentally succeed }); assert.equal(r.code, 1); assert.match(r.stdout + r.stderr, /no token available/); assert.ok(!existsSync(npmrc)); }); });