Files
redsen-lean-harness/tests/setup-npm-registry.test.mjs
T
mozempk cf13066baa feat(setup-npm-registry): browser-assisted classic PAT creation
When no token is found via --token/env/gh CLI, the script now opens
github.com/settings/tokens/new pre-scoped to read:packages with clear
instructions on Note/Expiration/Scopes, falling back to printing the
URL when a browser can't be launched (SSH, containers, CI). Add
--no-open to skip the launch attempt outright.

Also document the flow in README (new 'Generating a GitHub token'
section) and QUICKSTART troubleshooting, and add a hermetic test
covering the --no-open + piped-token path.
2026-09-10 01:44:28 +02:00

132 lines
5.3 KiB
JavaScript

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));
});
});