hivecast-sdk/packages/cli/tests/integration/fp30-auth-publish.spec.ts

590 lines
20 KiB
TypeScript
Raw Normal View History

import { afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
import { createMatrixPackage } from '../helpers/package-fixtures.js';
interface INpmPublishRegistryFixture {
readonly registryUrl: string;
readonly close: () => Promise<void>;
readonly publishRequests: () => number;
readonly tarballRequests: () => number;
readonly authorizationHeaders: () => readonly string[];
}
interface IPublishedPackage {
readonly name: string;
readonly version: string;
readonly fileName: string;
readonly tarball: Buffer;
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function readString(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value : null;
}
function addressPort(server: http.Server): number {
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Fixture registry did not bind a TCP port');
}
return address.port;
}
async function readRequestBody(req: http.IncomingMessage): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks).toString('utf8');
}
async function startNpmPublishRegistryFixture(): Promise<INpmPublishRegistryFixture> {
let publishRequests = 0;
let tarballRequests = 0;
let published: IPublishedPackage | null = null;
const authorizationHeaders: string[] = [];
const server = http.createServer((req, res) => {
void (async () => {
const auth = req.headers.authorization;
if (typeof auth === 'string') {
authorizationHeaders.push(auth);
}
const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
const url = new URL(req.url ?? '/', registryUrl);
if (req.method === 'PUT') {
publishRequests += 1;
const body = asRecord(JSON.parse(await readRequestBody(req)) as unknown);
const packageName = readString(body?.name);
const distTags = asRecord(body?.['dist-tags']);
const version = readString(distTags?.latest);
const attachments = asRecord(body?._attachments);
const attachmentEntry = attachments ? Object.entries(attachments)[0] : undefined;
const attachment = asRecord(attachmentEntry?.[1]);
const data = readString(attachment?.data);
if (!packageName || !version || !attachmentEntry || !data) {
res.writeHead(400, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'invalid publish payload' }));
return;
}
published = {
name: packageName,
version,
fileName: attachmentEntry[0],
tarball: Buffer.from(data, 'base64'),
};
res.writeHead(201, { 'content-type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === 'GET' && url.pathname.endsWith('.tgz')) {
tarballRequests += 1;
if (!published) {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'no tarball published' }));
return;
}
res.writeHead(200, { 'content-type': 'application/octet-stream' });
res.end(published.tarball);
return;
}
const decodedPath = decodeURIComponent(url.pathname);
if (req.method === 'GET' && published && decodedPath === `/${published.name}`) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
name: published.name,
'dist-tags': { latest: published.version },
versions: {
[published.version]: {
name: published.name,
version: published.version,
dist: {
tarball: new URL(`tarballs/${published.fileName}`, registryUrl).toString(),
},
},
},
}));
return;
}
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not found', path: url.pathname }));
})().catch((error: unknown) => {
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
});
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
return {
registryUrl: `http://127.0.0.1:${addressPort(server)}/`,
close: async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
publishRequests: () => publishRequests,
tarballRequests: () => tarballRequests,
authorizationHeaders: () => authorizationHeaders,
};
}
describe('FP-30 mx auth + publish integration', { concurrency: false }, () => {
const tempDirs: string[] = [];
const restoreEnvFns: Array<() => void> = [];
afterEach(() => {
while (restoreEnvFns.length > 0) {
const restore = restoreEnvFns.pop();
if (restore) {
restore();
}
}
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});
function useMatrixHome(matrixHome: string): void {
const previousMatrixHome = process.env.MATRIX_HOME;
const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG;
const previousNodeAuthToken = process.env.NODE_AUTH_TOKEN;
const previousNpmToken = process.env.NPM_TOKEN;
process.env.MATRIX_HOME = matrixHome;
delete process.env.MATRIX_CLI_CONFIG;
delete process.env.MATRIX_PACKAGE_REGISTRY;
delete process.env.MATRIX_REGISTRY_URL;
delete process.env.NPM_CONFIG_USERCONFIG;
delete process.env.NODE_AUTH_TOKEN;
delete process.env.NPM_TOKEN;
restoreEnvFns.push(() => {
restoreEnv('MATRIX_HOME', previousMatrixHome);
restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig);
restoreEnv('NODE_AUTH_TOKEN', previousNodeAuthToken);
restoreEnv('NPM_TOKEN', previousNpmToken);
});
}
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key];
return;
}
process.env[key] = value;
}
it('requires login before publish and enables registry install after publish', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-auth-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const registryDir = path.join(root, '.matrix', 'registry');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const homeDir = path.join(root, 'home');
const previousHome = process.env.HOME;
fs.mkdirSync(homeDir, { recursive: true });
process.env.HOME = homeDir;
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-auth-publish',
version: '1.0.0',
componentType: 'AuthPublishActor',
mount: 'auth.publish',
});
const publishWithoutLogin = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishWithoutLogin, 1);
assert.equal(publishWithoutLogin.errors.some((line) => line.includes('MX_AUTH_REQUIRED')), true);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-auth-publish',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry-dir',
registryDir,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const installFromRegistry = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry-dir',
registryDir,
'--json',
], { cwd: workspaceDir });
assertExitCode(installFromRegistry, 0);
const payload = parseLastJson(installFromRegistry.logs) as { targetDir: string };
assert.equal(fs.existsSync(payload.targetDir), true);
} finally {
restoreEnv('HOME', previousHome);
}
});
it('publishes to an npm registry API and installs from the published tarball', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npm-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const registry = await startNpmPublishRegistryFixture();
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-npm-publish',
version: '1.0.0',
componentType: 'NpmPublishActor',
mount: 'npm.publish',
});
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-npm-publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryUrl?: string;
};
assert.equal(publishPayload.packageName, fixture.packageName);
assert.equal(publishPayload.version, fixture.version);
assert.equal(publishPayload.registryUrl, registry.registryUrl);
assert.equal(registry.publishRequests(), 1);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-npm-publish')),
true
);
const installResult = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(installResult, 0);
const installPayload = parseLastJson(installResult.logs) as { targetDir: string };
assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true);
assert.equal(registry.tarballRequests(), 1);
} finally {
await registry.close();
}
});
it('keeps Matrix scoped packages on the requested npm registry API', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-scoped-npm-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
const homeDir = path.join(root, 'home');
fs.mkdirSync(workspaceDir, { recursive: true });
fs.mkdirSync(homeDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const registry = await startNpmPublishRegistryFixture();
const previousHome = process.env.HOME;
const previousGlobalConfig = process.env.NPM_CONFIG_GLOBALCONFIG;
const wrongRegistry = 'http://127.0.0.1:9/wrong/npm/';
const poisonedGlobalConfig = path.join(root, 'poisoned-global-npmrc');
fs.writeFileSync(
path.join(homeDir, '.npmrc'),
[
`@matrix:registry=${wrongRegistry}`,
`@open-matrix:registry=${wrongRegistry}`,
`@omega:registry=${wrongRegistry}`,
'',
].join('\n'),
'utf8',
);
fs.writeFileSync(
poisonedGlobalConfig,
[
`@matrix:registry=${wrongRegistry}`,
`@open-matrix:registry=${wrongRegistry}`,
`@omega:registry=${wrongRegistry}`,
'',
].join('\n'),
'utf8',
);
process.env.HOME = homeDir;
process.env.NPM_CONFIG_GLOBALCONFIG = poisonedGlobalConfig;
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@matrix/fp30-scoped-npm-publish',
version: '1.0.0',
componentType: 'ScopedNpmPublishActor',
mount: 'scoped.npm.publish',
});
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-scoped-npm-publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
assert.equal(registry.publishRequests(), 1);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-scoped-npm-publish')),
true,
);
const installResult = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(installResult, 0);
const installPayload = parseLastJson(installResult.logs) as { targetDir: string };
assert.equal(fs.existsSync(path.join(installPayload.targetDir, 'matrix.json')), true);
assert.equal(registry.tarballRequests(), 1);
} finally {
restoreEnv('HOME', previousHome);
restoreEnv('NPM_CONFIG_GLOBALCONFIG', previousGlobalConfig);
await registry.close();
}
});
it('publishes to the configured npm registry API without a per-command registry flag', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-configured-npm-publish-'));
tempDirs.push(root);
useMatrixHome(path.join(root, '.matrix'));
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
const registry = await startNpmPublishRegistryFixture();
try {
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-configured-npm-publish',
version: '1.0.0',
componentType: 'ConfiguredNpmPublishActor',
mount: 'configured.npm.publish',
});
const setConfig = await runMxCli([
'node',
'mx',
'config',
'set',
'registry',
registry.registryUrl,
'--json',
], { cwd: workspaceDir });
assertExitCode(setConfig, 0);
const loginResult = await runMxCli([
'node',
'mx',
'login',
'--username',
'alice',
'--token',
'token-configured-npm-publish',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: workspaceDir });
assertExitCode(loginResult, 0);
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--credentials-file',
credentialsFile,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const publishPayload = parseLastJson(publishResult.logs) as {
packageName: string;
version: string;
registryUrl?: string;
};
assert.equal(publishPayload.packageName, fixture.packageName);
assert.equal(publishPayload.version, fixture.version);
assert.equal(publishPayload.registryUrl, registry.registryUrl);
assert.equal(registry.publishRequests(), 1);
assert.equal(
registry.authorizationHeaders().some((header) => header.includes('token-configured-npm-publish')),
true,
);
} finally {
await registry.close();
}
});
it('publishes to an npm registry API using an npmrc token', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-npmrc-publish-'));
tempDirs.push(root);
const workspaceDir = path.join(root, 'workspace');
fs.mkdirSync(workspaceDir, { recursive: true });
const packagesDir = path.join(root, '.matrix', 'packages');
const fixture = createMatrixPackage(path.join(root, 'source'), {
packageName: '@acme/fp30-npmrc-publish',
version: '1.0.0',
componentType: 'NpmrcPublishActor',
mount: 'npmrc.publish',
});
const registry = await startNpmPublishRegistryFixture();
const npmrcPath = path.join(root, '.npmrc');
const registryKey = registry.registryUrl.replace(/^https?:/, '');
const previousNpmUserConfig = process.env.NPM_CONFIG_USERCONFIG;
restoreEnvFns.push(() => restoreEnv('NPM_CONFIG_USERCONFIG', previousNpmUserConfig));
fs.writeFileSync(
npmrcPath,
`${registryKey}:_authToken=npmrc-token-publish\n`,
'utf8',
);
process.env.NPM_CONFIG_USERCONFIG = npmrcPath;
try {
const publishResult = await runMxCli([
'node',
'mx',
'publish',
'--registry',
registry.registryUrl,
'--json',
], { cwd: fixture.packageDir });
assertExitCode(publishResult, 0);
const installResult = await runMxCli([
'node',
'mx',
'install',
`${fixture.packageName}@${fixture.version}`,
'--packages-dir',
packagesDir,
'--registry',
registry.registryUrl,
'--json',
], { cwd: workspaceDir });
assertExitCode(installResult, 0);
assert.equal(registry.publishRequests(), 1);
assert.equal(registry.tarballRequests(), 1);
assert.equal(registry.authorizationHeaders().includes('Bearer npmrc-token-publish'), true);
} finally {
await registry.close();
}
});
});