165 lines
6.7 KiB
TypeScript
165 lines
6.7 KiB
TypeScript
|
|
/**
|
||
|
|
* Smoke tests for the BUILT bundles in dist/.
|
||
|
|
*
|
||
|
|
* Every other suite runs against src/ through ts-jest, where the platform
|
||
|
|
* modules resolve as ordinary relative paths. That masked a bug in which the
|
||
|
|
* published package loaded fine but threw "Cannot find module './node'" as soon
|
||
|
|
* as anything touched the platform layer — rollup flattens the modules, so the
|
||
|
|
* runtime require had nothing to resolve. The client could not be constructed
|
||
|
|
* at all from the npm package.
|
||
|
|
*
|
||
|
|
* These tests exercise the artefact that actually ships. Run `npm run build`
|
||
|
|
* first; they skip themselves if dist/ is absent.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import * as fs from 'fs';
|
||
|
|
import * as path from 'path';
|
||
|
|
import { spawnSync } from 'child_process';
|
||
|
|
|
||
|
|
const DIST = path.join(__dirname, '..', '..', 'dist');
|
||
|
|
const NODE_CJS = path.join(DIST, 'node', 'index.js');
|
||
|
|
const BROWSER_CJS = path.join(DIST, 'browser', 'index.cjs');
|
||
|
|
const ESM_ENTRY = path.join(DIST, 'esm', 'index.mjs');
|
||
|
|
|
||
|
|
const built = fs.existsSync(NODE_CJS);
|
||
|
|
const describeIfBuilt = built ? describe : describe.skip;
|
||
|
|
|
||
|
|
if (!built) {
|
||
|
|
// eslint-disable-next-line no-console
|
||
|
|
console.warn('dist/ not found — run `npm run build` to exercise the bundle smoke tests');
|
||
|
|
}
|
||
|
|
|
||
|
|
interface NomyoModule {
|
||
|
|
SecureChatCompletion: new (config?: object) => { dispose(): void };
|
||
|
|
SecureCompletionClient: new (config: object) => { dispose(): void };
|
||
|
|
getMemoryProtectionInfo: () => { method: string; canLock: boolean };
|
||
|
|
AttestationPolicy: new (options?: object) => { enforce: boolean };
|
||
|
|
SecurityError: new (message: string) => Error;
|
||
|
|
}
|
||
|
|
|
||
|
|
describeIfBuilt('built Node bundle', () => {
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||
|
|
const nomyo = require(NODE_CJS) as NomyoModule;
|
||
|
|
|
||
|
|
test('exposes the public API', () => {
|
||
|
|
expect(typeof nomyo.SecureChatCompletion).toBe('function');
|
||
|
|
expect(typeof nomyo.SecureCompletionClient).toBe('function');
|
||
|
|
expect(typeof nomyo.getMemoryProtectionInfo).toBe('function');
|
||
|
|
expect(typeof nomyo.AttestationPolicy).toBe('function');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('constructs a client — the platform layer resolves', () => {
|
||
|
|
// This is the exact call that threw "Cannot find module './node'"
|
||
|
|
const client = new nomyo.SecureChatCompletion({});
|
||
|
|
expect(client).toBeTruthy();
|
||
|
|
client.dispose();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('constructs the low-level client too', () => {
|
||
|
|
const client = new nomyo.SecureCompletionClient({
|
||
|
|
routerUrl: 'https://router.test',
|
||
|
|
keyRotationInterval: 0,
|
||
|
|
});
|
||
|
|
expect(client).toBeTruthy();
|
||
|
|
client.dispose();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('reaches the real secure-memory implementation', () => {
|
||
|
|
const info = nomyo.getMemoryProtectionInfo();
|
||
|
|
expect(['mlock', 'zero-only', 'none']).toContain(info.method);
|
||
|
|
expect(typeof info.canLock).toBe('boolean');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('carries no unresolved runtime requires', () => {
|
||
|
|
const source = fs.readFileSync(NODE_CJS, 'utf-8');
|
||
|
|
// './node' / './browser' only ever resolved pre-bundling
|
||
|
|
expect(source).not.toMatch(/require\(['"]\.\/node['"]\)/);
|
||
|
|
expect(source).not.toMatch(/require\(['"]\.\/browser['"]\)/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('attestation policy survives bundling', () => {
|
||
|
|
const policy = new nomyo.AttestationPolicy({ enforce: true });
|
||
|
|
expect(policy.enforce).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describeIfBuilt('built browser bundle', () => {
|
||
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||
|
|
const nomyo = require(BROWSER_CJS) as NomyoModule;
|
||
|
|
|
||
|
|
test('exposes the public API', () => {
|
||
|
|
expect(typeof nomyo.SecureChatCompletion).toBe('function');
|
||
|
|
expect(typeof nomyo.getMemoryProtectionInfo).toBe('function');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('constructs a client without a DOM', () => {
|
||
|
|
const client = new nomyo.SecureChatCompletion({});
|
||
|
|
expect(client).toBeTruthy();
|
||
|
|
client.dispose();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('reports browser memory protection', () => {
|
||
|
|
const info = nomyo.getMemoryProtectionInfo();
|
||
|
|
expect(info.canLock).toBe(false);
|
||
|
|
expect(info.method).toBe('zero-only');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('carries no unresolved runtime requires', () => {
|
||
|
|
const source = fs.readFileSync(BROWSER_CJS, 'utf-8');
|
||
|
|
expect(source).not.toMatch(/require\(['"]\.\/node['"]\)/);
|
||
|
|
expect(source).not.toMatch(/require\(['"]\.\/browser['"]\)/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('does not drag the Node platform layer into the browser build', () => {
|
||
|
|
const source = fs.readFileSync(BROWSER_CJS, 'utf-8');
|
||
|
|
// The Node HTTP client and the optional native addon must not appear.
|
||
|
|
// `fs`/`path` legitimately do: KeyManager has Node-only branches guarded
|
||
|
|
// by `typeof window`, shared by both builds.
|
||
|
|
for (const nodeOnly of ['https', 'http', 'url', 'nomyo-native', 'node-gyp-build']) {
|
||
|
|
expect(source).not.toMatch(
|
||
|
|
new RegExp(`require\\(['"]${nodeOnly}['"]\\)`)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describeIfBuilt('built ESM entry point', () => {
|
||
|
|
test('is real ESM with an unambiguous extension', () => {
|
||
|
|
// A .js ES module in a non-"type":"module" package is parsed as
|
||
|
|
// CommonJS by Node and fails on older versions
|
||
|
|
expect(fs.existsSync(ESM_ENTRY)).toBe(true);
|
||
|
|
expect(fs.existsSync(path.join(DIST, 'esm', 'index.js'))).toBe(false);
|
||
|
|
expect(fs.readFileSync(ESM_ENTRY, 'utf-8')).toMatch(/^export |[\n;]export /);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('shims require() for its lazily-loaded modules', () => {
|
||
|
|
// The source loads crypto/fs/path/jose/the native addon via require().
|
||
|
|
// `require` is not defined in ES module scope, so the bundle must
|
||
|
|
// provide it or those paths throw ReferenceError at runtime.
|
||
|
|
const source = fs.readFileSync(ESM_ENTRY, 'utf-8');
|
||
|
|
if (/[^.\w]require\(/.test(source)) {
|
||
|
|
expect(source).toMatch(/createRequire/);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('is importable and usable by a real Node process', () => {
|
||
|
|
// Not `await import(...)` — ts-jest compiles dynamic import down to
|
||
|
|
// require() for its CommonJS target, which cannot load .mjs and would
|
||
|
|
// test the compiler rather than the bundle. Spawn Node instead.
|
||
|
|
const script = `import(${JSON.stringify(ESM_ENTRY)})`
|
||
|
|
+ '.then(m => {'
|
||
|
|
+ ' const c = new m.SecureChatCompletion({}); c.dispose();'
|
||
|
|
// Touches the require()-dependent crypto path
|
||
|
|
+ ' if (typeof m.getMemoryProtectionInfo().method !== "string") process.exit(2);'
|
||
|
|
+ '})'
|
||
|
|
+ '.catch(e => { console.error(e.message); process.exit(3); })';
|
||
|
|
|
||
|
|
const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
|
||
|
|
encoding: 'utf-8',
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result.stderr.trim()).toBe('');
|
||
|
|
expect(result.status).toBe(0);
|
||
|
|
});
|
||
|
|
});
|