import { describe, expect, it } from 'vitest'; import { checkLockfilePolicy } from './dependency-policy.mjs'; import { rebuildArgs, validateRebuildEntries, verify } from './rebuild-native.mjs'; const registryOrigin = 'https://registry.npmjs.org/'; describe('dependency policy adversarial cases', () => { it('rejects a non-link package when resolved and integrity are both missing', () => { const violations = checkLockfilePolicy( { lockfileVersion: 3, packages: { 'node_modules/evil': { version: '1.0.0', hasInstallScript: true, }, }, }, ['node_modules/evil'], registryOrigin, ); expect(violations).toContainEqual({ code: 'missing-resolved', name: 'node_modules/evil', }); expect(violations).toContainEqual({ code: 'missing-integrity', name: 'node_modules/evil', }); }); it('rejects lookalike registry hostnames', () => { const violations = checkLockfilePolicy( { lockfileVersion: 3, packages: { 'node_modules/evil': { version: '1.0.0', resolved: 'https://registry.npmjs.org.evil.example/evil.tgz', integrity: 'sha512-example', }, }, }, [], registryOrigin, ); expect(violations).toContainEqual({ code: 'non-registry-url', name: 'node_modules/evil', }); }); // このテストは名前で path / name / version / integrity の 4 つを縛ると言っている。 // ところが実際に起こしていたのは version の食い違いだけで、integrity 照合を // 削っても 35 件すべて緑だった(PR #1074 のレビュー M8)。4 つを別々に起こす。 const soundLockfile = (over = {}) => ({ lockfileVersion: 3, packages: { 'node_modules/native': { version: '1.0.0', resolved: 'https://registry.npmjs.org/native/-/native-1.0.0.tgz', integrity: 'sha512-approved', hasInstallScript: true, ...over, }, }, }); const soundApproval = (over = {}) => [{ path: 'node_modules/native', name: 'native', version: '1.0.0', integrity: 'sha512-approved', verifier: 'better-sqlite3-memory-db', ...over, }]; it('accepts an approval that matches the lockfile on every field', () => { expect(validateRebuildEntries(soundLockfile(), soundApproval())).toHaveLength(1); }); it('rejects an integrity mismatch even when the version agrees', () => { expect(() => validateRebuildEntries(soundLockfile({ integrity: 'sha512-swapped' }), soundApproval())) .toThrow(/integrity mismatch/i); }); it('rejects a target the lockfile says has no install script', () => { // 許可一覧に「script を持たない依存」を書けると、rebuild だけが走る経路になる expect(() => validateRebuildEntries(soundLockfile({ hasInstallScript: false }), soundApproval())) .toThrow(/no install script/i); }); it('rejects a path whose last node_modules segment is not the approved name', () => { // 経路の取り出しは lastIndexOf でなければならない。indexOf に変えると // node_modules/a/node_modules/b が "a/node_modules/b" になり、 // 名前の食い違いを見逃す expect(() => validateRebuildEntries( { lockfileVersion: 3, packages: { 'node_modules/outer/node_modules/native': { version: '1.0.0', resolved: 'https://registry.npmjs.org/native/-/native-1.0.0.tgz', integrity: 'sha512-approved', hasInstallScript: true, }, }, }, soundApproval({ path: 'node_modules/outer/node_modules/native', name: 'outer' }), )).toThrow(/name mismatch/i); }); it('accepts a nested path when the approved name matches its last segment', () => { expect(validateRebuildEntries( { lockfileVersion: 3, packages: { 'node_modules/outer/node_modules/native': { version: '1.0.0', resolved: 'https://registry.npmjs.org/native/-/native-1.0.0.tgz', integrity: 'sha512-approved', hasInstallScript: true, }, }, }, soundApproval({ path: 'node_modules/outer/node_modules/native' }), )).toHaveLength(1); }); it('refuses a verifier it does not know, instead of silently skipping', () => { // verify を即 return にしても 35 件すべて緑だった(レビュー M7)。 // 「実際にデータベースを開いて確かめる」が丸ごと消えても誰も気づかない状態。 expect(() => verify('no-such-verifier')).toThrow(/unsupported native rebuild verifier/i); }); it('actually opens a database for the verifier this repository uses', () => { // 中身が消えたら(即 return になったら)ここが空振りになるので、 // 検証が本当に走ったことを、走った先の失敗で見る expect(() => verify('better-sqlite3-memory-db')).not.toThrow(); }); it('binds native rebuild permission to path, name, version, and integrity', () => { const lockfile = { lockfileVersion: 3, packages: { 'node_modules/native': { version: '2.0.0', resolved: 'https://registry.npmjs.org/native/-/native-2.0.0.tgz', integrity: 'sha512-new', hasInstallScript: true, }, }, }; const approved = [{ path: 'node_modules/native', name: 'native', version: '1.0.0', integrity: 'sha512-approved', verifier: 'better-sqlite3-memory-db', }]; expect(() => validateRebuildEntries(lockfile, approved)).toThrow(/version mismatch/i); }); }); describe('how npm rebuild is invoked', () => { // 引数の並びに意味がある箇所。PR #1074 のレビューが、`--` を消しても // 35 件すべて緑だったと報告した(M6)。 it('separates the package name from flags with --', () => { const args = rebuildArgs('better-sqlite3'); expect(args[args.length - 2]).toBe('--'); expect(args[args.length - 1]).toBe('better-sqlite3'); }); it('builds from source instead of downloading a prebuilt binary', () => { // 配布物を取ってこないぶん安全側。消すと取得物が 1 つ増える expect(rebuildArgs('better-sqlite3')).toContain('--build-from-source'); }); });