/** * 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); } }); });