feat(opsserver,web): replace the Angular UI and REST management layer with a TypedRequest-based ops server and bundled web frontend

This commit is contained in:
2026-03-20 16:43:44 +00:00
parent 0fc74ff995
commit d4f758ce0f
159 changed files with 12465 additions and 14861 deletions

View File

@@ -1,18 +1,18 @@
version: "3.8"
version: '3.8'
services:
mongodb-test:
image: mongo:7
container_name: stack-gallery-test-mongo
ports:
- "27117:27017"
- '27117:27017'
environment:
MONGO_INITDB_ROOT_USERNAME: testadmin
MONGO_INITDB_ROOT_PASSWORD: testpass
tmpfs:
- /data/db
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
interval: 5s
timeout: 5s
retries: 5
@@ -21,8 +21,8 @@ services:
image: minio/minio:latest
container_name: stack-gallery-test-minio
ports:
- "9100:9000"
- "9101:9001"
- '9100:9000'
- '9101:9001'
environment:
MINIO_ROOT_USER: testadmin
MINIO_ROOT_PASSWORD: testpassword
@@ -30,7 +30,7 @@ services:
tmpfs:
- /data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
interval: 5s
timeout: 5s
retries: 5

View File

@@ -6,25 +6,25 @@
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import * as path from '@std/path';
import {
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
createOrgWithOwner,
createTestRepository,
createTestApiToken,
clients,
skipIfMissing,
createOrgWithOwner,
createTestApiToken,
createTestRepository,
createTestUser,
runCommand,
setupTestDb,
skipIfMissing,
teardownTestDb,
testConfig,
} from '../helpers/index.ts';
const FIXTURE_DIR = path.join(
path.dirname(path.fromFileUrl(import.meta.url)),
'../fixtures/npm/@stack-test/demo-package'
'../fixtures/npm/@stack-test/demo-package',
);
describe('NPM E2E: Full lifecycle', () => {
@@ -98,7 +98,7 @@ describe('NPM E2E: Full lifecycle', () => {
const result = await clients.npm.publish(
FIXTURE_DIR,
`${registryUrl}/-/npm/${testOrgName}/`,
apiToken
apiToken,
);
assertEquals(result.success, true, `npm publish failed: ${result.stderr}`);
@@ -120,20 +120,28 @@ describe('NPM E2E: Full lifecycle', () => {
// First publish
const npmrcPath = path.join(FIXTURE_DIR, '.npmrc');
const npmrcContent = `//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}`;
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
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 } }
[
'npm',
'view',
'@stack-test/demo-package',
'--registry',
`${registryUrl}/-/npm/${testOrgName}/`,
],
{ env: { npm_config__authToken: apiToken } },
);
assertEquals(viewResult.success, true, `npm view failed: ${viewResult.stderr}`);
@@ -159,32 +167,36 @@ describe('NPM E2E: Full lifecycle', () => {
try {
// First publish
const npmrcPath = path.join(FIXTURE_DIR, '.npmrc');
const npmrcContent = `//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}`;
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
apiToken,
);
// Create package.json in temp dir
await Deno.writeTextFile(
path.join(tempDir, 'package.json'),
JSON.stringify({ name: 'test-install', version: '1.0.0' })
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}`
`@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
tempDir,
);
assertEquals(installResult.success, true, `npm install failed: ${installResult.stderr}`);
@@ -213,33 +225,41 @@ describe('NPM E2E: Full lifecycle', () => {
// First publish
const npmrcPath = path.join(FIXTURE_DIR, '.npmrc');
const npmrcContent = `//${new URL(registryUrl).host}/-/npm/${testOrgName}/:_authToken=${apiToken}`;
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
apiToken,
);
// Unpublish
const unpublishResult = await clients.npm.unpublish(
'@stack-test/demo-package@1.0.0',
`${registryUrl}/-/npm/${testOrgName}/`,
apiToken
apiToken,
);
assertEquals(
unpublishResult.success,
true,
`npm unpublish failed: ${unpublishResult.stderr}`
`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 } }
[
'npm',
'view',
'@stack-test/demo-package',
'--registry',
`${registryUrl}/-/npm/${testOrgName}/`,
],
{ env: { npm_config__authToken: apiToken } },
);
// Should fail since package was unpublished

View File

@@ -6,24 +6,24 @@
*/
import { assertEquals } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import * as path from '@std/path';
import {
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
createOrgWithOwner,
createTestRepository,
createTestApiToken,
clients,
createOrgWithOwner,
createTestApiToken,
createTestRepository,
createTestUser,
setupTestDb,
skipIfMissing,
teardownTestDb,
testConfig,
} from '../helpers/index.ts';
const FIXTURE_DIR = path.join(
path.dirname(path.fromFileUrl(import.meta.url)),
'../fixtures/oci'
'../fixtures/oci',
);
describe('OCI E2E: Full lifecycle', () => {

View File

@@ -1,21 +1,21 @@
{
"name": "stacktest/demo-package",
"description": "Demo package for Stack.Gallery Registry e2e tests",
"version": "1.0.0",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Stack.Gallery Test",
"email": "test@stack.gallery"
}
],
"require": {
"php": ">=8.0"
},
"autoload": {
"psr-4": {
"StackTest\\DemoPackage\\": "src/"
}
"name": "stacktest/demo-package",
"description": "Demo package for Stack.Gallery Registry e2e tests",
"version": "1.0.0",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Stack.Gallery Test",
"email": "test@stack.gallery"
}
],
"require": {
"php": ">=8.0"
},
"autoload": {
"psr-4": {
"StackTest\\DemoPackage\\": "src/"
}
}
}

View File

@@ -1,34 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.stacktest</groupId>
<artifactId>demo-artifact</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Stack.Gallery Demo Artifact</name>
<description>Demo Maven artifact for e2e tests</description>
<url>https://github.com/stack-gallery/demo-artifact</url>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>4.0.0</modelVersion>
<groupId>com.stacktest</groupId>
<artifactId>demo-artifact</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Stack.Gallery Demo Artifact</name>
<description>Demo Maven artifact for e2e tests</description>
<url>https://github.com/stack-gallery/demo-artifact</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<developers>
<developer>
<name>Stack.Gallery Test</name>
<email>test@stack.gallery</email>
</developer>
</developers>
<developers>
<developer>
<name>Stack.Gallery Test</name>
<email>test@stack.gallery</email>
</developer>
</developers>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -5,5 +5,5 @@
module.exports = {
name: 'demo-package',
greet: (name) => `Hello, ${name}!`,
version: () => require('./package.json').version
version: () => require('./package.json').version,
};

View File

@@ -6,7 +6,11 @@ import { User } from '../../ts/models/user.ts';
import { ApiToken } from '../../ts/models/apitoken.ts';
import { AuthService } from '../../ts/services/auth.service.ts';
import { TokenService } from '../../ts/services/token.service.ts';
import type { TRegistryProtocol, ITokenScope, TUserStatus } from '../../ts/interfaces/auth.interfaces.ts';
import type {
ITokenScope,
TRegistryProtocol,
TUserStatus,
} from '../../ts/interfaces/auth.interfaces.ts';
import { testConfig } from '../test.config.ts';
const TEST_PASSWORD = 'TestPassword123!';
@@ -25,7 +29,7 @@ export interface ICreateTestUserOptions {
* Create a test user with sensible defaults
*/
export async function createTestUser(
overrides: ICreateTestUserOptions = {}
overrides: ICreateTestUserOptions = {},
): Promise<{ user: User; password: string }> {
const uniqueId = crypto.randomUUID().slice(0, 8);
const password = overrides.password || TEST_PASSWORD;
@@ -61,7 +65,7 @@ export async function createAdminUser(): Promise<{ user: User; password: string
*/
export async function loginUser(
email: string,
password: string
password: string,
): Promise<{ accessToken: string; refreshToken: string; sessionId: string }> {
const authService = new AuthService({
jwtSecret: testConfig.jwt.secret,
@@ -96,7 +100,7 @@ export interface ICreateTestApiTokenOptions {
* Create test API token
*/
export async function createTestApiToken(
options: ICreateTestApiTokenOptions
options: ICreateTestApiTokenOptions,
): Promise<{ rawToken: string; token: ApiToken }> {
const tokenService = new TokenService();
@@ -127,7 +131,7 @@ export function createAuthHeader(token: string): { Authorization: string } {
*/
export function createBasicAuthHeader(
username: string,
password: string
password: string,
): { Authorization: string } {
const credentials = btoa(`${username}:${password}`);
return { Authorization: `Basic ${credentials}` };

View File

@@ -15,7 +15,7 @@ let isConnected = false;
// We need to patch the global db export since models reference it
// This is done by re-initializing with the test config
import { initDb, closeDb } from '../../ts/models/db.ts';
import { closeDb, initDb } from '../../ts/models/db.ts';
/**
* Initialize test database with unique name per test run

View File

@@ -11,10 +11,10 @@ import { Package } from '../../ts/models/package.ts';
import { RepositoryPermission } from '../../ts/models/repository.permission.ts';
import type {
TOrganizationRole,
TTeamRole,
TRegistryProtocol,
TRepositoryRole,
TRepositoryVisibility,
TRegistryProtocol,
TTeamRole,
} from '../../ts/interfaces/auth.interfaces.ts';
export interface ICreateTestOrganizationOptions {
@@ -29,7 +29,7 @@ export interface ICreateTestOrganizationOptions {
* Create test organization
*/
export async function createTestOrganization(
options: ICreateTestOrganizationOptions
options: ICreateTestOrganizationOptions,
): Promise<Organization> {
const uniqueId = crypto.randomUUID().slice(0, 8);
@@ -53,7 +53,7 @@ export async function createTestOrganization(
*/
export async function createOrgWithOwner(
ownerId: string,
orgOptions?: Partial<ICreateTestOrganizationOptions>
orgOptions?: Partial<ICreateTestOrganizationOptions>,
): Promise<{
organization: Organization;
membership: OrganizationMember;
@@ -82,7 +82,7 @@ export async function addOrgMember(
organizationId: string,
userId: string,
role: TOrganizationRole = 'member',
invitedBy?: string
invitedBy?: string,
): Promise<OrganizationMember> {
const membership = await OrganizationMember.addMember({
organizationId,
@@ -113,7 +113,7 @@ export interface ICreateTestRepositoryOptions {
* Create test repository
*/
export async function createTestRepository(
options: ICreateTestRepositoryOptions
options: ICreateTestRepositoryOptions,
): Promise<Repository> {
const uniqueId = crypto.randomUUID().slice(0, 8);
@@ -152,7 +152,7 @@ export async function createTestTeam(options: ICreateTestTeamOptions): Promise<T
export async function addTeamMember(
teamId: string,
userId: string,
role: TTeamRole = 'member'
role: TTeamRole = 'member',
): Promise<TeamMember> {
const member = new TeamMember();
member.id = await TeamMember.getNewId();
@@ -176,7 +176,7 @@ export interface IGrantRepoPermissionOptions {
* Grant repository permission
*/
export async function grantRepoPermission(
options: IGrantRepoPermissionOptions
options: IGrantRepoPermissionOptions,
): Promise<RepositoryPermission> {
const perm = new RepositoryPermission();
perm.id = await RepositoryPermission.getNewId();

View File

@@ -75,7 +75,7 @@ export const del = (path: string, headers?: Record<string, string>) =>
export function assertStatus(response: ITestResponse, expected: number): void {
if (response.status !== expected) {
throw new Error(
`Expected status ${expected} but got ${response.status}: ${JSON.stringify(response.body)}`
`Expected status ${expected} but got ${response.status}: ${JSON.stringify(response.body)}`,
);
}
}
@@ -98,7 +98,7 @@ export function assertBodyHas(response: ITestResponse, keys: string[]): void {
export function assertSuccess(response: ITestResponse): void {
if (response.status < 200 || response.status >= 300) {
throw new Error(
`Expected successful response but got ${response.status}: ${JSON.stringify(response.body)}`
`Expected successful response but got ${response.status}: ${JSON.stringify(response.body)}`,
);
}
}

View File

@@ -4,82 +4,82 @@
// Database helpers
export {
setupTestDb,
cleanupTestDb,
teardownTestDb,
clearCollections,
getTestDbName,
getTestDb,
getTestDbName,
setupTestDb,
teardownTestDb,
} from './db.helper.ts';
// Auth helpers
export {
createTestUser,
createAdminUser,
loginUser,
createTestApiToken,
createAuthHeader,
createBasicAuthHeader,
createTestApiToken,
createTestUser,
getTestPassword,
type ICreateTestUserOptions,
type ICreateTestApiTokenOptions,
type ICreateTestUserOptions,
loginUser,
} from './auth.helper.ts';
// Factory helpers
export {
createTestOrganization,
createOrgWithOwner,
addOrgMember,
addTeamMember,
createFullTestScenario,
createOrgWithOwner,
createTestOrganization,
createTestPackage,
createTestRepository,
createTestTeam,
addTeamMember,
grantRepoPermission,
createTestPackage,
createFullTestScenario,
type ICreateTestOrganizationOptions,
type ICreateTestPackageOptions,
type ICreateTestRepositoryOptions,
type ICreateTestTeamOptions,
type IGrantRepoPermissionOptions,
type ICreateTestPackageOptions,
} from './factory.helper.ts';
// HTTP helpers
export {
testRequest,
get,
post,
put,
patch,
del,
assertStatus,
assertBodyHas,
assertSuccess,
assertError,
assertStatus,
assertSuccess,
del,
get,
type ITestRequest,
type ITestResponse,
patch,
post,
put,
testRequest,
} from './http.helper.ts';
// Subprocess helpers
export {
runCommand,
commandExists,
clients,
skipIfMissing,
type ICommandResult,
commandExists,
type ICommandOptions,
type ICommandResult,
runCommand,
skipIfMissing,
} from './subprocess.helper.ts';
// Storage helpers
export {
setupTestStorage,
checkStorageAvailable,
objectExists,
listObjects,
cleanupTestStorage,
deleteObject,
deletePrefix,
cleanupTestStorage,
isStorageAvailable,
listObjects,
objectExists,
setupTestStorage,
} from './storage.helper.ts';
// Re-export test config
export { testConfig, getTestConfig } from '../test.config.ts';
export { getTestConfig, testConfig } from '../test.config.ts';

View File

@@ -22,7 +22,7 @@ export interface ICommandOptions {
*/
export async function runCommand(
cmd: string[],
options: ICommandOptions = {}
options: ICommandOptions = {},
): Promise<ICommandResult> {
const { cwd, env, timeout = 60000, stdin } = options;
@@ -116,7 +116,7 @@ export const clients = {
publish: (dir: string, registry: string, token: string) =>
runCommand(
['cargo', 'publish', '--registry', 'stack-test', '--token', token, '--allow-dirty'],
{ cwd: dir }
{ cwd: dir },
),
yank: (crate: string, version: string, token: string) =>
runCommand([
@@ -164,7 +164,7 @@ export const clients = {
'--repository',
JSON.stringify({ type: 'composer', url: repository }),
],
{ cwd: dir }
{ cwd: dir },
),
},
@@ -190,7 +190,7 @@ export const clients = {
`-Dusername=${username}`,
`-Dpassword=${password}`,
],
{ cwd: dir }
{ cwd: dir },
),
package: (dir: string) => runCommand(['mvn', 'package', '-DskipTests'], { cwd: dir }),
},

View File

@@ -4,16 +4,16 @@
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import {
assertStatus,
cleanupTestDb,
createAuthHeader,
createTestUser,
get,
post,
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
post,
get,
assertStatus,
createAuthHeader,
} from '../helpers/index.ts';
describe('Auth API Integration', () => {
@@ -126,7 +126,7 @@ describe('Auth API Integration', () => {
// Get current user
const meResponse = await get(
'/api/v1/auth/me',
createAuthHeader(loginBody.accessToken as string)
createAuthHeader(loginBody.accessToken as string),
);
assertStatus(meResponse, 200);

View File

@@ -4,19 +4,19 @@
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import {
setupTestDb,
teardownTestDb,
assertStatus,
cleanupTestDb,
createAuthHeader,
createTestUser,
del,
get,
loginUser,
post,
get,
put,
del,
assertStatus,
createAuthHeader,
setupTestDb,
teardownTestDb,
} from '../helpers/index.ts';
describe('Organization API Integration', () => {
@@ -48,7 +48,7 @@ describe('Organization API Integration', () => {
displayName: 'My Organization',
description: 'A test organization',
},
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 201);
@@ -64,7 +64,7 @@ describe('Organization API Integration', () => {
name: 'push.rocks',
displayName: 'Push Rocks',
},
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 201);
@@ -76,13 +76,13 @@ describe('Organization API Integration', () => {
await post(
'/api/v1/organizations',
{ name: 'duplicate', displayName: 'First' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
const response = await post(
'/api/v1/organizations',
{ name: 'duplicate', displayName: 'Second' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 409);
@@ -92,7 +92,7 @@ describe('Organization API Integration', () => {
const response = await post(
'/api/v1/organizations',
{ name: '.invalid', displayName: 'Invalid' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 400);
@@ -105,12 +105,12 @@ describe('Organization API Integration', () => {
await post(
'/api/v1/organizations',
{ name: 'org1', displayName: 'Org 1' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
await post(
'/api/v1/organizations',
{ name: 'org2', displayName: 'Org 2' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
const response = await get('/api/v1/organizations', createAuthHeader(accessToken));
@@ -126,7 +126,7 @@ describe('Organization API Integration', () => {
await post(
'/api/v1/organizations',
{ name: 'get-me', displayName: 'Get Me' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
const response = await get('/api/v1/organizations/get-me', createAuthHeader(accessToken));
@@ -139,7 +139,7 @@ describe('Organization API Integration', () => {
it('should return 404 for non-existent org', async () => {
const response = await get(
'/api/v1/organizations/non-existent',
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 404);
@@ -151,13 +151,13 @@ describe('Organization API Integration', () => {
await post(
'/api/v1/organizations',
{ name: 'update-me', displayName: 'Original' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
const response = await put(
'/api/v1/organizations/update-me',
{ displayName: 'Updated', description: 'New description' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 200);
@@ -172,7 +172,7 @@ describe('Organization API Integration', () => {
await post(
'/api/v1/organizations',
{ name: 'delete-me', displayName: 'Delete Me' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
const response = await del('/api/v1/organizations/delete-me', createAuthHeader(accessToken));
@@ -182,7 +182,7 @@ describe('Organization API Integration', () => {
// Verify deleted
const getResponse = await get(
'/api/v1/organizations/delete-me',
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(getResponse, 404);
});
@@ -193,12 +193,12 @@ describe('Organization API Integration', () => {
await post(
'/api/v1/organizations',
{ name: 'members-org', displayName: 'Members Org' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
const response = await get(
'/api/v1/organizations/members-org/members',
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 200);
@@ -213,13 +213,13 @@ describe('Organization API Integration', () => {
await post(
'/api/v1/organizations',
{ name: 'add-member-org', displayName: 'Add Member Org' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
const response = await post(
'/api/v1/organizations/add-member-org/members',
{ userId: newUser.id, role: 'member' },
createAuthHeader(accessToken)
createAuthHeader(accessToken),
);
assertStatus(response, 201);

View File

@@ -7,10 +7,10 @@ import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', '.nogit/', false);
const mongoUrl = await testQenv.getEnvVarOnDemand('MONGODB_URL')
|| 'mongodb://testadmin:testpass@localhost:27117/test-registry?authSource=admin';
const mongoName = await testQenv.getEnvVarOnDemand('MONGODB_NAME')
|| 'test-registry';
const mongoUrl = await testQenv.getEnvVarOnDemand('MONGODB_URL') ||
'mongodb://testadmin:testpass@localhost:27117/test-registry?authSource=admin';
const mongoName = await testQenv.getEnvVarOnDemand('MONGODB_NAME') ||
'test-registry';
const s3Endpoint = await testQenv.getEnvVarOnDemand('S3_ENDPOINT') || 'localhost';
const s3Port = await testQenv.getEnvVarOnDemand('S3_PORT') || '9100';

View File

@@ -3,8 +3,8 @@
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import { cleanupTestDb, createTestUser, setupTestDb, teardownTestDb } from '../../helpers/index.ts';
import { ApiToken } from '../../../ts/models/apitoken.ts';
describe('ApiToken Model', () => {

View File

@@ -3,8 +3,8 @@
*/
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import { cleanupTestDb, createTestUser, setupTestDb, teardownTestDb } from '../../helpers/index.ts';
import { Organization } from '../../../ts/models/organization.ts';
describe('Organization Model', () => {
@@ -73,7 +73,7 @@ describe('Organization Model', () => {
});
},
Error,
'lowercase alphanumeric'
'lowercase alphanumeric',
);
});
@@ -87,7 +87,7 @@ describe('Organization Model', () => {
});
},
Error,
'lowercase alphanumeric'
'lowercase alphanumeric',
);
});
@@ -101,7 +101,7 @@ describe('Organization Model', () => {
});
},
Error,
'lowercase alphanumeric'
'lowercase alphanumeric',
);
});
@@ -115,7 +115,7 @@ describe('Organization Model', () => {
});
},
Error,
'lowercase alphanumeric'
'lowercase alphanumeric',
);
});

View File

@@ -3,14 +3,14 @@
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import {
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
createOrgWithOwner,
createTestRepository,
createTestUser,
setupTestDb,
teardownTestDb,
} from '../../helpers/index.ts';
import { Package } from '../../../ts/models/package.ts';
import type { IPackageVersion } from '../../../ts/interfaces/package.interfaces.ts';

View File

@@ -3,13 +3,13 @@
*/
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import {
cleanupTestDb,
createOrgWithOwner,
createTestUser,
setupTestDb,
teardownTestDb,
cleanupTestDb,
createTestUser,
createOrgWithOwner,
} from '../../helpers/index.ts';
import { Repository } from '../../../ts/models/repository.ts';
@@ -103,7 +103,7 @@ describe('Repository Model', () => {
});
},
Error,
'already exists'
'already exists',
);
});
@@ -137,7 +137,7 @@ describe('Repository Model', () => {
});
},
Error,
'lowercase alphanumeric'
'lowercase alphanumeric',
);
});

View File

@@ -3,8 +3,8 @@
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import { cleanupTestDb, createTestUser, setupTestDb, teardownTestDb } from '../../helpers/index.ts';
import { Session } from '../../../ts/models/session.ts';
describe('Session Model', () => {
@@ -70,9 +70,21 @@ describe('Session Model', () => {
describe('getUserSessions', () => {
it('should return all valid sessions for user', async () => {
await Session.createSession({ userId: testUserId, userAgent: 'Agent 1', ipAddress: '1.1.1.1' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 2', ipAddress: '2.2.2.2' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 3', ipAddress: '3.3.3.3' });
await Session.createSession({
userId: testUserId,
userAgent: 'Agent 1',
ipAddress: '1.1.1.1',
});
await Session.createSession({
userId: testUserId,
userAgent: 'Agent 2',
ipAddress: '2.2.2.2',
});
await Session.createSession({
userId: testUserId,
userAgent: 'Agent 3',
ipAddress: '3.3.3.3',
});
const sessions = await Session.getUserSessions(testUserId);
assertEquals(sessions.length, 3);
@@ -110,9 +122,21 @@ describe('Session Model', () => {
describe('invalidateAllUserSessions', () => {
it('should invalidate all user sessions', async () => {
await Session.createSession({ userId: testUserId, userAgent: 'Agent 1', ipAddress: '1.1.1.1' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 2', ipAddress: '2.2.2.2' });
await Session.createSession({ userId: testUserId, userAgent: 'Agent 3', ipAddress: '3.3.3.3' });
await Session.createSession({
userId: testUserId,
userAgent: 'Agent 1',
ipAddress: '1.1.1.1',
});
await Session.createSession({
userId: testUserId,
userAgent: 'Agent 2',
ipAddress: '2.2.2.2',
});
await Session.createSession({
userId: testUserId,
userAgent: 'Agent 3',
ipAddress: '3.3.3.3',
});
const count = await Session.invalidateAllUserSessions(testUserId, 'Security logout');
assertEquals(count, 3);

View File

@@ -3,8 +3,8 @@
*/
import { assertEquals, assertExists, assertRejects } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb } from '../../helpers/index.ts';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import { cleanupTestDb, setupTestDb, teardownTestDb } from '../../helpers/index.ts';
import { User } from '../../../ts/models/user.ts';
describe('User Model', () => {

View File

@@ -3,8 +3,8 @@
*/
import { assertEquals, assertExists } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import { cleanupTestDb, createTestUser, setupTestDb, teardownTestDb } from '../../helpers/index.ts';
import { AuthService } from '../../../ts/services/auth.service.ts';
import { Session } from '../../../ts/models/session.ts';
import { testConfig } from '../../test.config.ts';

View File

@@ -3,8 +3,8 @@
*/
import { assertEquals, assertExists, assertMatch } from 'jsr:@std/assert';
import { describe, it, beforeAll, afterAll, beforeEach } from 'jsr:@std/testing/bdd';
import { setupTestDb, teardownTestDb, cleanupTestDb, createTestUser } from '../../helpers/index.ts';
import { afterAll, beforeAll, beforeEach, describe, it } from 'jsr:@std/testing/bdd';
import { cleanupTestDb, createTestUser, setupTestDb, teardownTestDb } from '../../helpers/index.ts';
import { TokenService } from '../../../ts/services/token.service.ts';
import { ApiToken } from '../../../ts/models/apitoken.ts';
@@ -39,8 +39,8 @@ describe('TokenService', () => {
assertExists(result.rawToken);
assertExists(result.token);
// Check token format: srg_{prefix}_{random}
assertMatch(result.rawToken, /^srg_[a-z0-9]+_[a-z0-9]+$/);
// Check token format: srg_ + 64 hex chars
assertMatch(result.rawToken, /^srg_[a-f0-9]{64}$/);
assertEquals(result.token.name, 'test-token');
assertEquals(result.token.protocols.includes('npm'), true);
assertEquals(result.token.protocols.includes('oci'), true);
@@ -111,13 +111,19 @@ describe('TokenService', () => {
it('should reject invalid token format', async () => {
const validation = await tokenService.validateToken('invalid-format', '127.0.0.1');
assertEquals(validation, null);
assertEquals(validation.valid, false);
assertEquals(validation.errorCode, 'INVALID_TOKEN_FORMAT');
});
it('should reject non-existent token', async () => {
const validation = await tokenService.validateToken('srg_abc123_def456', '127.0.0.1');
// Must match srg_ prefix + 64 hex chars = 68 total
const validation = await tokenService.validateToken(
'srg_0000000000000000000000000000000000000000000000000000000000000000',
'127.0.0.1',
);
assertEquals(validation, null);
assertEquals(validation.valid, false);
assertEquals(validation.errorCode, 'TOKEN_NOT_FOUND');
});
it('should reject revoked token', async () => {
@@ -132,7 +138,9 @@ describe('TokenService', () => {
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
assertEquals(validation, null);
assertEquals(validation.valid, false);
// findByHash excludes revoked tokens, so the token is not found
assertEquals(validation.errorCode, 'TOKEN_NOT_FOUND');
});
it('should reject expired token', async () => {
@@ -150,7 +158,8 @@ describe('TokenService', () => {
const validation = await tokenService.validateToken(rawToken, '127.0.0.1');
assertEquals(validation, null);
assertEquals(validation.valid, false);
assertEquals(validation.errorCode, 'TOKEN_EXPIRED');
});
it('should record usage on validation', async () => {