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.
132 lines
5.3 KiB
JavaScript
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));
|
|
});
|
|
});
|