Add unit tests for models and services
- 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.
This commit is contained in:
290
test/e2e/npm.e2e.test.ts
Normal file
290
test/e2e/npm.e2e.test.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
190
test/e2e/oci.e2e.test.ts
Normal file
190
test/e2e/oci.e2e.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* OCI Protocol E2E Tests
|
||||
*
|
||||
* Tests the full OCI container image lifecycle: push -> pull -> delete
|
||||
* Requires: docker CLI, running registry, Docker test infrastructure
|
||||
*/
|
||||
|
||||
import { assertEquals } 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,
|
||||
testConfig,
|
||||
} from '../helpers/index.ts';
|
||||
|
||||
const FIXTURE_DIR = path.join(
|
||||
path.dirname(path.fromFileUrl(import.meta.url)),
|
||||
'../fixtures/oci'
|
||||
);
|
||||
|
||||
describe('OCI E2E: Full lifecycle', () => {
|
||||
let testUserId: string;
|
||||
let testOrgName: string;
|
||||
let apiToken: string;
|
||||
let registryHost: string;
|
||||
let shouldSkip = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Check if docker is available
|
||||
shouldSkip = await skipIfMissing('docker');
|
||||
if (shouldSkip) return;
|
||||
|
||||
await setupTestDb();
|
||||
const url = new URL(testConfig.registry.url);
|
||||
registryHost = url.host;
|
||||
});
|
||||
|
||||
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: 'oci-test' });
|
||||
testOrgName = organization.name;
|
||||
|
||||
// Create repository for OCI images
|
||||
await createTestRepository({
|
||||
organizationId: organization.id,
|
||||
createdById: testUserId,
|
||||
name: 'images',
|
||||
protocol: 'oci',
|
||||
});
|
||||
|
||||
// Create API token with OCI permissions
|
||||
const { rawToken } = await createTestApiToken({
|
||||
userId: testUserId,
|
||||
name: 'oci-push-token',
|
||||
protocols: ['oci'],
|
||||
scopes: [{ protocol: 'oci', actions: ['read', 'write', 'delete'] }],
|
||||
});
|
||||
apiToken = rawToken;
|
||||
});
|
||||
|
||||
it('should build and push image', async function () {
|
||||
if (shouldSkip) {
|
||||
console.log('Skipping: docker not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageName = `${registryHost}/v2/${testOrgName}/demo:1.0.0`;
|
||||
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.simple');
|
||||
|
||||
try {
|
||||
// Build image
|
||||
const buildResult = await clients.docker.build(dockerfile, imageName, FIXTURE_DIR);
|
||||
assertEquals(buildResult.success, true, `docker build failed: ${buildResult.stderr}`);
|
||||
|
||||
// Login to registry
|
||||
const loginResult = await clients.docker.login(registryHost, 'token', apiToken);
|
||||
assertEquals(loginResult.success, true, `docker login failed: ${loginResult.stderr}`);
|
||||
|
||||
// Push image
|
||||
const pushResult = await clients.docker.push(imageName);
|
||||
assertEquals(pushResult.success, true, `docker push failed: ${pushResult.stderr}`);
|
||||
} finally {
|
||||
// Cleanup local image
|
||||
await clients.docker.rmi(imageName, true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should pull image', async function () {
|
||||
if (shouldSkip) {
|
||||
console.log('Skipping: docker not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageName = `${registryHost}/v2/${testOrgName}/demo:1.0.0`;
|
||||
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.simple');
|
||||
|
||||
try {
|
||||
// Build and push first
|
||||
await clients.docker.build(dockerfile, imageName, FIXTURE_DIR);
|
||||
await clients.docker.login(registryHost, 'token', apiToken);
|
||||
await clients.docker.push(imageName);
|
||||
|
||||
// Remove local image
|
||||
await clients.docker.rmi(imageName, true);
|
||||
|
||||
// Pull from registry
|
||||
const pullResult = await clients.docker.pull(imageName);
|
||||
assertEquals(pullResult.success, true, `docker pull failed: ${pullResult.stderr}`);
|
||||
} finally {
|
||||
await clients.docker.rmi(imageName, true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle multi-layer images', async function () {
|
||||
if (shouldSkip) {
|
||||
console.log('Skipping: docker not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageName = `${registryHost}/v2/${testOrgName}/multi:1.0.0`;
|
||||
const dockerfile = path.join(FIXTURE_DIR, 'Dockerfile.multi-layer');
|
||||
|
||||
try {
|
||||
// Build multi-stage image
|
||||
const buildResult = await clients.docker.build(dockerfile, imageName, FIXTURE_DIR);
|
||||
assertEquals(buildResult.success, true, `docker build failed: ${buildResult.stderr}`);
|
||||
|
||||
// Login and push
|
||||
await clients.docker.login(registryHost, 'token', apiToken);
|
||||
const pushResult = await clients.docker.push(imageName);
|
||||
assertEquals(pushResult.success, true, `docker push failed: ${pushResult.stderr}`);
|
||||
} finally {
|
||||
await clients.docker.rmi(imageName, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('OCI E2E: Tags and versions', () => {
|
||||
let shouldSkip = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
shouldSkip = await skipIfMissing('docker');
|
||||
});
|
||||
|
||||
it('should handle multiple tags for same image', async function () {
|
||||
if (shouldSkip) {
|
||||
console.log('Skipping: docker not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify tag handling logic
|
||||
const tags = ['1.0.0', '1.0', '1', 'latest'];
|
||||
for (const tag of tags) {
|
||||
assertEquals(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(tag), true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle SHA256 digests', async function () {
|
||||
if (shouldSkip) {
|
||||
console.log('Skipping: docker not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify digest format
|
||||
const digest = 'sha256:' + 'a'.repeat(64);
|
||||
assertEquals(digest.startsWith('sha256:'), true);
|
||||
assertEquals(digest.length, 71);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user