- Implemented unit tests for the Package model, covering methods such as generateId, findById, findByName, and version management. - Created unit tests for the Repository model, including repository creation, name validation, and retrieval methods. - Added tests for the Session model, focusing on session creation, validation, and invalidation. - Developed unit tests for the User model, ensuring user creation, password hashing, and retrieval methods function correctly. - Implemented AuthService tests, validating login, token refresh, and session management. - Added TokenService tests, covering token creation, validation, and revocation processes.
291 lines
7.8 KiB
TypeScript
291 lines
7.8 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
});
|
|
});
|