Files
registry/test/e2e/npm.e2e.test.ts

291 lines
7.8 KiB
TypeScript
Raw Normal View History

/**
* NPM Protocol E2E Tests
*
* Tests the full NPM package lifecycle: publish -> fetch -> delete
* Requires: npm CLI, running registry, Docker test infrastructure
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import * as path from '@std/path';
import {
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
createOrgWithOwner,
createTestRepository,
createTestApiToken,
clients,
skipIfMissing,
runCommand,
testConfig,
} from '../helpers/index.ts';
const FIXTURE_DIR = path.join(
path.dirname(path.fromFileUrl(import.meta.url)),
'../fixtures/npm/@stack-test/demo-package'
);
describe('NPM E2E: Full lifecycle', () => {
let testUserId: string;
let testOrgName: string;
let apiToken: string;
let registryUrl: string;
let shouldSkip = false;
beforeAll(async () => {
// Check if npm is available
shouldSkip = await skipIfMissing('npm');
if (shouldSkip) return;
await setupTestDb();
registryUrl = testConfig.registry.url;
});
afterAll(async () => {
if (!shouldSkip) {
await teardownTestDb();
}
});
beforeEach(async () => {
if (shouldSkip) return;
await cleanupTestDb();
// Create test user and org
const { user } = await createTestUser({ status: 'active' });
testUserId = user.id;
const { organization } = await createOrgWithOwner(testUserId, { name: 'npm-test' });
testOrgName = organization.name;
// Create repository for npm packages
await createTestRepository({
organizationId: organization.id,
createdById: testUserId,
name: 'packages',
protocol: 'npm',
});
// Create API token with npm permissions
const { rawToken } = await createTestApiToken({
userId: testUserId,
name: 'npm-publish-token',
protocols: ['npm'],
scopes: [{ protocol: 'npm', actions: ['read', 'write', 'delete'] }],
});
apiToken = rawToken;
});
it('should publish package', async function () {
if (shouldSkip) {
console.log('Skipping: npm not available');
return;
}
// Configure npm to use our registry
const npmrcPath = path.join(FIXTURE_DIR, '.npmrc');
const npmrcContent = `
//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}
@stack-test:registry=${registryUrl}/-/npm/${testOrgName}/
`;
await Deno.writeTextFile(npmrcPath, npmrcContent);
try {
const result = await clients.npm.publish(
FIXTURE_DIR,
`${registryUrl}/-/npm/${testOrgName}/`,
apiToken
);
assertEquals(result.success, true, `npm publish failed: ${result.stderr}`);
} finally {
// Cleanup .npmrc
try {
await Deno.remove(npmrcPath);
} catch {
// Ignore
}
}
});
it('should fetch package metadata', async function () {
if (shouldSkip) {
console.log('Skipping: npm not available');
return;
}
// First publish
const npmrcPath = path.join(FIXTURE_DIR, '.npmrc');
const npmrcContent = `//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}`;
await Deno.writeTextFile(npmrcPath, npmrcContent);
try {
await clients.npm.publish(
FIXTURE_DIR,
`${registryUrl}/-/npm/${testOrgName}/`,
apiToken
);
// Fetch metadata via npm view
const viewResult = await runCommand(
['npm', 'view', '@stack-test/demo-package', '--registry', `${registryUrl}/-/npm/${testOrgName}/`],
{ env: { npm_config__authToken: apiToken } }
);
assertEquals(viewResult.success, true, `npm view failed: ${viewResult.stderr}`);
assertEquals(viewResult.stdout.includes('@stack-test/demo-package'), true);
} finally {
try {
await Deno.remove(npmrcPath);
} catch {
// Ignore
}
}
});
it('should install package', async function () {
if (shouldSkip) {
console.log('Skipping: npm not available');
return;
}
// Create temp directory for installation
const tempDir = await Deno.makeTempDir({ prefix: 'npm-e2e-' });
try {
// First publish
const npmrcPath = path.join(FIXTURE_DIR, '.npmrc');
const npmrcContent = `//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}`;
await Deno.writeTextFile(npmrcPath, npmrcContent);
await clients.npm.publish(
FIXTURE_DIR,
`${registryUrl}/-/npm/${testOrgName}/`,
apiToken
);
// Create package.json in temp dir
await Deno.writeTextFile(
path.join(tempDir, 'package.json'),
JSON.stringify({ name: 'test-install', version: '1.0.0' })
);
// Create .npmrc in temp dir
await Deno.writeTextFile(
path.join(tempDir, '.npmrc'),
`@stack-test:registry=${registryUrl}/-/npm/${testOrgName}/\n//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}`
);
// Install
const installResult = await clients.npm.install(
'@stack-test/demo-package@1.0.0',
`${registryUrl}/-/npm/${testOrgName}/`,
tempDir
);
assertEquals(installResult.success, true, `npm install failed: ${installResult.stderr}`);
// Verify installed
const pkgPath = path.join(tempDir, 'node_modules/@stack-test/demo-package');
const stat = await Deno.stat(pkgPath);
assertEquals(stat.isDirectory, true);
// Cleanup fixture .npmrc
try {
await Deno.remove(npmrcPath);
} catch {
// Ignore
}
} finally {
await Deno.remove(tempDir, { recursive: true });
}
});
it('should unpublish package', async function () {
if (shouldSkip) {
console.log('Skipping: npm not available');
return;
}
// First publish
const npmrcPath = path.join(FIXTURE_DIR, '.npmrc');
const npmrcContent = `//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}`;
await Deno.writeTextFile(npmrcPath, npmrcContent);
try {
await clients.npm.publish(
FIXTURE_DIR,
`${registryUrl}/-/npm/${testOrgName}/`,
apiToken
);
// Unpublish
const unpublishResult = await clients.npm.unpublish(
'@stack-test/demo-package@1.0.0',
`${registryUrl}/-/npm/${testOrgName}/`,
apiToken
);
assertEquals(
unpublishResult.success,
true,
`npm unpublish failed: ${unpublishResult.stderr}`
);
// Verify package is gone
const viewResult = await runCommand(
['npm', 'view', '@stack-test/demo-package', '--registry', `${registryUrl}/-/npm/${testOrgName}/`],
{ env: { npm_config__authToken: apiToken } }
);
// Should fail since package was unpublished
assertEquals(viewResult.success, false);
} finally {
try {
await Deno.remove(npmrcPath);
} catch {
// Ignore
}
}
});
});
describe('NPM E2E: Edge cases', () => {
let shouldSkip = false;
beforeAll(async () => {
shouldSkip = await skipIfMissing('npm');
});
it('should handle scoped packages correctly', async function () {
if (shouldSkip) {
console.log('Skipping: npm not available');
return;
}
// Test scoped package name handling
const scopedName = '@stack-test/demo-package';
assertEquals(scopedName.startsWith('@'), true);
assertEquals(scopedName.includes('/'), true);
});
it('should reject invalid package names', async function () {
if (shouldSkip) {
console.log('Skipping: npm not available');
return;
}
// npm has strict naming rules
const invalidNames = ['UPPERCASE', '..dots..', 'spaces here', '_underscore'];
for (const name of invalidNames) {
// Just verify these are considered invalid by npm standards
assertEquals(!/^[a-z0-9][-a-z0-9._]*$/.test(name), true);
}
});
});