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:
141
test/helpers/auth.helper.ts
Normal file
141
test/helpers/auth.helper.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Authentication test helper - creates test users, tokens, and sessions
|
||||
*/
|
||||
|
||||
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 { testConfig } from '../test.config.ts';
|
||||
|
||||
const TEST_PASSWORD = 'TestPassword123!';
|
||||
|
||||
export interface ICreateTestUserOptions {
|
||||
email?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
displayName?: string;
|
||||
status?: TUserStatus;
|
||||
isPlatformAdmin?: boolean;
|
||||
emailVerified?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test user with sensible defaults
|
||||
*/
|
||||
export async function createTestUser(
|
||||
overrides: ICreateTestUserOptions = {}
|
||||
): Promise<{ user: User; password: string }> {
|
||||
const uniqueId = crypto.randomUUID().slice(0, 8);
|
||||
const password = overrides.password || TEST_PASSWORD;
|
||||
const passwordHash = await User.hashPassword(password);
|
||||
|
||||
const user = await User.createUser({
|
||||
email: overrides.email || `test-${uniqueId}@example.com`,
|
||||
username: overrides.username || `testuser-${uniqueId}`,
|
||||
passwordHash,
|
||||
displayName: overrides.displayName || `Test User ${uniqueId}`,
|
||||
});
|
||||
|
||||
// Set additional properties
|
||||
user.status = overrides.status || 'active';
|
||||
user.emailVerified = overrides.emailVerified ?? true;
|
||||
if (overrides.isPlatformAdmin) {
|
||||
user.isPlatformAdmin = true;
|
||||
}
|
||||
await user.save();
|
||||
|
||||
return { user, password };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create admin user
|
||||
*/
|
||||
export async function createAdminUser(): Promise<{ user: User; password: string }> {
|
||||
return createTestUser({ isPlatformAdmin: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Login and get tokens
|
||||
*/
|
||||
export async function loginUser(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ accessToken: string; refreshToken: string; sessionId: string }> {
|
||||
const authService = new AuthService({
|
||||
jwtSecret: testConfig.jwt.secret,
|
||||
});
|
||||
|
||||
const result = await authService.login(email, password, {
|
||||
userAgent: 'TestAgent/1.0',
|
||||
ipAddress: '127.0.0.1',
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Login failed: ${result.errorMessage}`);
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: result.accessToken!,
|
||||
refreshToken: result.refreshToken!,
|
||||
sessionId: result.sessionId!,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICreateTestApiTokenOptions {
|
||||
userId: string;
|
||||
name?: string;
|
||||
protocols?: TRegistryProtocol[];
|
||||
scopes?: ITokenScope[];
|
||||
organizationId?: string;
|
||||
expiresInDays?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test API token
|
||||
*/
|
||||
export async function createTestApiToken(
|
||||
options: ICreateTestApiTokenOptions
|
||||
): Promise<{ rawToken: string; token: ApiToken }> {
|
||||
const tokenService = new TokenService();
|
||||
|
||||
return tokenService.createToken({
|
||||
userId: options.userId,
|
||||
organizationId: options.organizationId,
|
||||
name: options.name || `test-token-${crypto.randomUUID().slice(0, 8)}`,
|
||||
protocols: options.protocols || ['npm', 'oci'],
|
||||
scopes: options.scopes || [
|
||||
{
|
||||
protocol: '*',
|
||||
actions: ['read', 'write', 'delete'],
|
||||
},
|
||||
],
|
||||
expiresInDays: options.expiresInDays,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create auth header for API requests
|
||||
*/
|
||||
export function createAuthHeader(token: string): { Authorization: string } {
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create basic auth header (for registry protocols)
|
||||
*/
|
||||
export function createBasicAuthHeader(
|
||||
username: string,
|
||||
password: string
|
||||
): { Authorization: string } {
|
||||
const credentials = btoa(`${username}:${password}`);
|
||||
return { Authorization: `Basic ${credentials}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default test password
|
||||
*/
|
||||
export function getTestPassword(): string {
|
||||
return TEST_PASSWORD;
|
||||
}
|
||||
106
test/helpers/db.helper.ts
Normal file
106
test/helpers/db.helper.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Database test helper - manages test database lifecycle
|
||||
*
|
||||
* NOTE: The smartdata models use a global `db` singleton. This helper
|
||||
* ensures proper initialization and cleanup for tests.
|
||||
*/
|
||||
|
||||
import * as plugins from '../../ts/plugins.ts';
|
||||
import { testConfig } from '../test.config.ts';
|
||||
|
||||
// Test database instance - separate from production
|
||||
let testDb: plugins.smartdata.SmartdataDb | null = null;
|
||||
let testDbName: string = '';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Initialize test database with unique name per test run
|
||||
*/
|
||||
export async function setupTestDb(config?: {
|
||||
mongoUrl?: string;
|
||||
dbName?: string;
|
||||
}): Promise<void> {
|
||||
// If already connected, reuse the connection
|
||||
if (isConnected && testDb) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mongoUrl = config?.mongoUrl || testConfig.mongodb.url;
|
||||
|
||||
// Generate unique database name for this test session
|
||||
const uniqueId = crypto.randomUUID().slice(0, 8);
|
||||
testDbName = config?.dbName || `${testConfig.mongodb.name}-${uniqueId}`;
|
||||
|
||||
// Initialize the global db singleton with test configuration
|
||||
testDb = await initDb(mongoUrl, testDbName);
|
||||
isConnected = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up test database - deletes all documents from collections
|
||||
* This is safer than dropping collections which causes index rebuild issues
|
||||
*/
|
||||
export async function cleanupTestDb(): Promise<void> {
|
||||
if (!testDb || !isConnected) return;
|
||||
|
||||
try {
|
||||
const collections = await testDb.mongoDb.listCollections().toArray();
|
||||
for (const col of collections) {
|
||||
// Delete all documents but preserve indexes
|
||||
await testDb.mongoDb.collection(col.name).deleteMany({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[TestHelper] Error cleaning database:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teardown test database - drops database and closes connection
|
||||
*/
|
||||
export async function teardownTestDb(): Promise<void> {
|
||||
if (!testDb || !isConnected) return;
|
||||
|
||||
try {
|
||||
// Drop the test database
|
||||
await testDb.mongoDb.dropDatabase();
|
||||
// Close the connection
|
||||
await closeDb();
|
||||
testDb = null;
|
||||
isConnected = false;
|
||||
} catch (error) {
|
||||
console.warn('[TestHelper] Error tearing down database:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear specific collection(s) - deletes all documents
|
||||
*/
|
||||
export async function clearCollections(...collectionNames: string[]): Promise<void> {
|
||||
if (!testDb || !isConnected) return;
|
||||
|
||||
for (const name of collectionNames) {
|
||||
try {
|
||||
await testDb.mongoDb.collection(name).deleteMany({});
|
||||
} catch {
|
||||
// Collection may not exist, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current test database name
|
||||
*/
|
||||
export function getTestDbName(): string {
|
||||
return testDbName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database instance for direct access
|
||||
*/
|
||||
export function getTestDb(): plugins.smartdata.SmartdataDb | null {
|
||||
return testDb;
|
||||
}
|
||||
268
test/helpers/factory.helper.ts
Normal file
268
test/helpers/factory.helper.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Factory helper - creates test entities with sensible defaults
|
||||
*/
|
||||
|
||||
import { Organization } from '../../ts/models/organization.ts';
|
||||
import { OrganizationMember } from '../../ts/models/organization.member.ts';
|
||||
import { Repository } from '../../ts/models/repository.ts';
|
||||
import { Team } from '../../ts/models/team.ts';
|
||||
import { TeamMember } from '../../ts/models/team.member.ts';
|
||||
import { Package } from '../../ts/models/package.ts';
|
||||
import { RepositoryPermission } from '../../ts/models/repository.permission.ts';
|
||||
import type {
|
||||
TOrganizationRole,
|
||||
TTeamRole,
|
||||
TRepositoryRole,
|
||||
TRepositoryVisibility,
|
||||
TRegistryProtocol,
|
||||
} from '../../ts/interfaces/auth.interfaces.ts';
|
||||
|
||||
export interface ICreateTestOrganizationOptions {
|
||||
createdById: string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test organization
|
||||
*/
|
||||
export async function createTestOrganization(
|
||||
options: ICreateTestOrganizationOptions
|
||||
): Promise<Organization> {
|
||||
const uniqueId = crypto.randomUUID().slice(0, 8);
|
||||
|
||||
const org = await Organization.createOrganization({
|
||||
name: options.name || `test-org-${uniqueId}`,
|
||||
displayName: options.displayName || `Test Org ${uniqueId}`,
|
||||
description: options.description || 'Test organization',
|
||||
createdById: options.createdById,
|
||||
});
|
||||
|
||||
if (options.isPublic !== undefined) {
|
||||
org.isPublic = options.isPublic;
|
||||
await org.save();
|
||||
}
|
||||
|
||||
return org;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create organization with owner membership
|
||||
*/
|
||||
export async function createOrgWithOwner(
|
||||
ownerId: string,
|
||||
orgOptions?: Partial<ICreateTestOrganizationOptions>
|
||||
): Promise<{
|
||||
organization: Organization;
|
||||
membership: OrganizationMember;
|
||||
}> {
|
||||
const organization = await createTestOrganization({
|
||||
createdById: ownerId,
|
||||
...orgOptions,
|
||||
});
|
||||
|
||||
const membership = await OrganizationMember.addMember({
|
||||
organizationId: organization.id,
|
||||
userId: ownerId,
|
||||
role: 'owner',
|
||||
});
|
||||
|
||||
organization.memberCount = 1;
|
||||
await organization.save();
|
||||
|
||||
return { organization, membership };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add member to organization
|
||||
*/
|
||||
export async function addOrgMember(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
role: TOrganizationRole = 'member',
|
||||
invitedBy?: string
|
||||
): Promise<OrganizationMember> {
|
||||
const membership = await OrganizationMember.addMember({
|
||||
organizationId,
|
||||
userId,
|
||||
role,
|
||||
invitedBy,
|
||||
});
|
||||
|
||||
const org = await Organization.findById(organizationId);
|
||||
if (org) {
|
||||
org.memberCount += 1;
|
||||
await org.save();
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
export interface ICreateTestRepositoryOptions {
|
||||
organizationId: string;
|
||||
createdById: string;
|
||||
name?: string;
|
||||
protocol?: TRegistryProtocol;
|
||||
visibility?: TRepositoryVisibility;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test repository
|
||||
*/
|
||||
export async function createTestRepository(
|
||||
options: ICreateTestRepositoryOptions
|
||||
): Promise<Repository> {
|
||||
const uniqueId = crypto.randomUUID().slice(0, 8);
|
||||
|
||||
return Repository.createRepository({
|
||||
organizationId: options.organizationId,
|
||||
name: options.name || `test-repo-${uniqueId}`,
|
||||
protocol: options.protocol || 'npm',
|
||||
visibility: options.visibility || 'private',
|
||||
description: options.description || 'Test repository',
|
||||
createdById: options.createdById,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ICreateTestTeamOptions {
|
||||
organizationId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test team
|
||||
*/
|
||||
export async function createTestTeam(options: ICreateTestTeamOptions): Promise<Team> {
|
||||
const uniqueId = crypto.randomUUID().slice(0, 8);
|
||||
|
||||
return Team.createTeam({
|
||||
organizationId: options.organizationId,
|
||||
name: options.name || `test-team-${uniqueId}`,
|
||||
description: options.description || 'Test team',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add member to team
|
||||
*/
|
||||
export async function addTeamMember(
|
||||
teamId: string,
|
||||
userId: string,
|
||||
role: TTeamRole = 'member'
|
||||
): Promise<TeamMember> {
|
||||
const member = new TeamMember();
|
||||
member.id = await TeamMember.getNewId();
|
||||
member.teamId = teamId;
|
||||
member.userId = userId;
|
||||
member.role = role;
|
||||
member.createdAt = new Date();
|
||||
await member.save();
|
||||
return member;
|
||||
}
|
||||
|
||||
export interface IGrantRepoPermissionOptions {
|
||||
repositoryId: string;
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
role: TRepositoryRole;
|
||||
grantedById: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant repository permission
|
||||
*/
|
||||
export async function grantRepoPermission(
|
||||
options: IGrantRepoPermissionOptions
|
||||
): Promise<RepositoryPermission> {
|
||||
const perm = new RepositoryPermission();
|
||||
perm.id = await RepositoryPermission.getNewId();
|
||||
perm.repositoryId = options.repositoryId;
|
||||
perm.userId = options.userId;
|
||||
perm.teamId = options.teamId;
|
||||
perm.role = options.role;
|
||||
perm.grantedById = options.grantedById;
|
||||
perm.createdAt = new Date();
|
||||
await perm.save();
|
||||
return perm;
|
||||
}
|
||||
|
||||
export interface ICreateTestPackageOptions {
|
||||
organizationId: string;
|
||||
repositoryId: string;
|
||||
createdById: string;
|
||||
name?: string;
|
||||
protocol?: TRegistryProtocol;
|
||||
versions?: string[];
|
||||
isPrivate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test package
|
||||
*/
|
||||
export async function createTestPackage(options: ICreateTestPackageOptions): Promise<Package> {
|
||||
const uniqueId = crypto.randomUUID().slice(0, 8);
|
||||
const protocol = options.protocol || 'npm';
|
||||
const name = options.name || `test-package-${uniqueId}`;
|
||||
|
||||
const pkg = new Package();
|
||||
pkg.id = Package.generateId(protocol, options.organizationId, name);
|
||||
pkg.organizationId = options.organizationId;
|
||||
pkg.repositoryId = options.repositoryId;
|
||||
pkg.protocol = protocol;
|
||||
pkg.name = name;
|
||||
pkg.isPrivate = options.isPrivate ?? true;
|
||||
pkg.createdById = options.createdById;
|
||||
pkg.createdAt = new Date();
|
||||
pkg.updatedAt = new Date();
|
||||
|
||||
const versions = options.versions || ['1.0.0'];
|
||||
for (const version of versions) {
|
||||
pkg.addVersion({
|
||||
version,
|
||||
publishedAt: new Date(),
|
||||
publishedById: options.createdById,
|
||||
size: 1024,
|
||||
digest: `sha256:${crypto.randomUUID().replace(/-/g, '')}`,
|
||||
downloads: 0,
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
|
||||
pkg.distTags['latest'] = versions[versions.length - 1];
|
||||
await pkg.save();
|
||||
return pkg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create complete test scenario with org, repo, team, and package
|
||||
*/
|
||||
export async function createFullTestScenario(ownerId: string): Promise<{
|
||||
organization: Organization;
|
||||
repository: Repository;
|
||||
team: Team;
|
||||
package: Package;
|
||||
}> {
|
||||
const { organization } = await createOrgWithOwner(ownerId);
|
||||
|
||||
const repository = await createTestRepository({
|
||||
organizationId: organization.id,
|
||||
createdById: ownerId,
|
||||
protocol: 'npm',
|
||||
});
|
||||
|
||||
const team = await createTestTeam({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
|
||||
const pkg = await createTestPackage({
|
||||
organizationId: organization.id,
|
||||
repositoryId: repository.id,
|
||||
createdById: ownerId,
|
||||
});
|
||||
|
||||
return { organization, repository, team, package: pkg };
|
||||
}
|
||||
116
test/helpers/http.helper.ts
Normal file
116
test/helpers/http.helper.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* HTTP test helper - utilities for testing API endpoints
|
||||
*/
|
||||
|
||||
import { testConfig } from '../test.config.ts';
|
||||
|
||||
export interface ITestRequest {
|
||||
method: string;
|
||||
path: string;
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
query?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ITestResponse {
|
||||
status: number;
|
||||
body: unknown;
|
||||
headers: Headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a test request to the registry API
|
||||
*/
|
||||
export async function testRequest(options: ITestRequest): Promise<ITestResponse> {
|
||||
const baseUrl = testConfig.registry.url;
|
||||
let url = `${baseUrl}${options.path}`;
|
||||
|
||||
if (options.query) {
|
||||
const params = new URLSearchParams(options.query);
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: options.method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
body = await response.text();
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
body,
|
||||
headers: response.headers,
|
||||
};
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
export const get = (path: string, headers?: Record<string, string>) =>
|
||||
testRequest({ method: 'GET', path, headers });
|
||||
|
||||
export const post = (path: string, body?: unknown, headers?: Record<string, string>) =>
|
||||
testRequest({ method: 'POST', path, body, headers });
|
||||
|
||||
export const put = (path: string, body?: unknown, headers?: Record<string, string>) =>
|
||||
testRequest({ method: 'PUT', path, body, headers });
|
||||
|
||||
export const patch = (path: string, body?: unknown, headers?: Record<string, string>) =>
|
||||
testRequest({ method: 'PATCH', path, body, headers });
|
||||
|
||||
export const del = (path: string, headers?: Record<string, string>) =>
|
||||
testRequest({ method: 'DELETE', path, headers });
|
||||
|
||||
/**
|
||||
* Assert response status
|
||||
*/
|
||||
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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert response body has specific keys
|
||||
*/
|
||||
export function assertBodyHas(response: ITestResponse, keys: string[]): void {
|
||||
const body = response.body as Record<string, unknown>;
|
||||
for (const key of keys) {
|
||||
if (!(key in body)) {
|
||||
throw new Error(`Expected response to have key "${key}", body: ${JSON.stringify(body)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert response is successful (2xx)
|
||||
*/
|
||||
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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert response is an error (4xx or 5xx)
|
||||
*/
|
||||
export function assertError(response: ITestResponse, expectedStatus?: number): void {
|
||||
if (response.status < 400) {
|
||||
throw new Error(`Expected error response but got ${response.status}`);
|
||||
}
|
||||
if (expectedStatus !== undefined && response.status !== expectedStatus) {
|
||||
throw new Error(`Expected status ${expectedStatus} but got ${response.status}`);
|
||||
}
|
||||
}
|
||||
85
test/helpers/index.ts
Normal file
85
test/helpers/index.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Test helpers index - re-exports all helper modules
|
||||
*/
|
||||
|
||||
// Database helpers
|
||||
export {
|
||||
setupTestDb,
|
||||
cleanupTestDb,
|
||||
teardownTestDb,
|
||||
clearCollections,
|
||||
getTestDbName,
|
||||
getTestDb,
|
||||
} from './db.helper.ts';
|
||||
|
||||
// Auth helpers
|
||||
export {
|
||||
createTestUser,
|
||||
createAdminUser,
|
||||
loginUser,
|
||||
createTestApiToken,
|
||||
createAuthHeader,
|
||||
createBasicAuthHeader,
|
||||
getTestPassword,
|
||||
type ICreateTestUserOptions,
|
||||
type ICreateTestApiTokenOptions,
|
||||
} from './auth.helper.ts';
|
||||
|
||||
// Factory helpers
|
||||
export {
|
||||
createTestOrganization,
|
||||
createOrgWithOwner,
|
||||
addOrgMember,
|
||||
createTestRepository,
|
||||
createTestTeam,
|
||||
addTeamMember,
|
||||
grantRepoPermission,
|
||||
createTestPackage,
|
||||
createFullTestScenario,
|
||||
type ICreateTestOrganizationOptions,
|
||||
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,
|
||||
type ITestRequest,
|
||||
type ITestResponse,
|
||||
} from './http.helper.ts';
|
||||
|
||||
// Subprocess helpers
|
||||
export {
|
||||
runCommand,
|
||||
commandExists,
|
||||
clients,
|
||||
skipIfMissing,
|
||||
type ICommandResult,
|
||||
type ICommandOptions,
|
||||
} from './subprocess.helper.ts';
|
||||
|
||||
// Storage helpers
|
||||
export {
|
||||
setupTestStorage,
|
||||
checkStorageAvailable,
|
||||
objectExists,
|
||||
listObjects,
|
||||
deleteObject,
|
||||
deletePrefix,
|
||||
cleanupTestStorage,
|
||||
isStorageAvailable,
|
||||
} from './storage.helper.ts';
|
||||
|
||||
// Re-export test config
|
||||
export { testConfig, getTestConfig } from '../test.config.ts';
|
||||
104
test/helpers/storage.helper.ts
Normal file
104
test/helpers/storage.helper.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Storage helper - S3/MinIO verification utilities for tests
|
||||
*
|
||||
* NOTE: These are stub implementations for testing.
|
||||
* The actual smartbucket API should be verified against the real library.
|
||||
*/
|
||||
|
||||
import { testConfig } from '../test.config.ts';
|
||||
|
||||
// Storage is optional for unit/integration tests
|
||||
// E2E tests with actual S3 operations would need proper implementation
|
||||
let storageAvailable = false;
|
||||
|
||||
/**
|
||||
* Check if test storage is available
|
||||
*/
|
||||
export async function checkStorageAvailable(): Promise<boolean> {
|
||||
try {
|
||||
// Try to connect to MinIO
|
||||
const response = await fetch(`${testConfig.s3.endpoint}/minio/health/live`, {
|
||||
method: 'GET',
|
||||
});
|
||||
storageAvailable = response.ok;
|
||||
return storageAvailable;
|
||||
} catch {
|
||||
storageAvailable = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize test storage connection
|
||||
*/
|
||||
export async function setupTestStorage(): Promise<void> {
|
||||
await checkStorageAvailable();
|
||||
if (storageAvailable) {
|
||||
console.log('[Test Storage] MinIO available at', testConfig.s3.endpoint);
|
||||
} else {
|
||||
console.log('[Test Storage] MinIO not available, storage tests will be skipped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an object exists in storage (stub)
|
||||
*/
|
||||
export async function objectExists(_key: string): Promise<boolean> {
|
||||
if (!storageAvailable) return false;
|
||||
// Would implement actual check here
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* List objects with a given prefix (stub)
|
||||
*/
|
||||
export async function listObjects(_prefix: string): Promise<string[]> {
|
||||
if (!storageAvailable) return [];
|
||||
// Would implement actual list here
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object from storage (stub)
|
||||
*/
|
||||
export async function deleteObject(_key: string): Promise<void> {
|
||||
if (!storageAvailable) return;
|
||||
// Would implement actual delete here
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all objects with a given prefix
|
||||
*/
|
||||
export async function deletePrefix(prefix: string): Promise<void> {
|
||||
const objects = await listObjects(prefix);
|
||||
for (const key of objects) {
|
||||
await deleteObject(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up test storage
|
||||
*/
|
||||
export async function cleanupTestStorage(): Promise<void> {
|
||||
if (!storageAvailable) return;
|
||||
|
||||
try {
|
||||
// Delete all test objects
|
||||
await deletePrefix('npm/');
|
||||
await deletePrefix('oci/');
|
||||
await deletePrefix('maven/');
|
||||
await deletePrefix('cargo/');
|
||||
await deletePrefix('pypi/');
|
||||
await deletePrefix('composer/');
|
||||
await deletePrefix('rubygems/');
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is available
|
||||
*/
|
||||
export function isStorageAvailable(): boolean {
|
||||
return storageAvailable;
|
||||
}
|
||||
208
test/helpers/subprocess.helper.ts
Normal file
208
test/helpers/subprocess.helper.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Subprocess helper - utilities for running protocol clients in tests
|
||||
*/
|
||||
|
||||
export interface ICommandResult {
|
||||
success: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
signal?: Deno.Signal;
|
||||
}
|
||||
|
||||
export interface ICommandOptions {
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stdin?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command and return the result
|
||||
*/
|
||||
export async function runCommand(
|
||||
cmd: string[],
|
||||
options: ICommandOptions = {}
|
||||
): Promise<ICommandResult> {
|
||||
const { cwd, env, timeout = 60000, stdin } = options;
|
||||
|
||||
const command = new Deno.Command(cmd[0], {
|
||||
args: cmd.slice(1),
|
||||
cwd,
|
||||
env: { ...Deno.env.toObject(), ...env },
|
||||
stdin: stdin ? 'piped' : 'null',
|
||||
stdout: 'piped',
|
||||
stderr: 'piped',
|
||||
});
|
||||
|
||||
const child = command.spawn();
|
||||
|
||||
if (stdin && child.stdin) {
|
||||
const writer = child.stdin.getWriter();
|
||||
await writer.write(new TextEncoder().encode(stdin));
|
||||
await writer.close();
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
try {
|
||||
child.kill('SIGTERM');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
const output = await child.output();
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
return {
|
||||
success: output.success,
|
||||
stdout: new TextDecoder().decode(output.stdout),
|
||||
stderr: new TextDecoder().decode(output.stderr),
|
||||
code: output.code,
|
||||
signal: output.signal ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command is available
|
||||
*/
|
||||
export async function commandExists(cmd: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await runCommand(['which', cmd], { timeout: 5000 });
|
||||
return result.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Protocol client wrappers
|
||||
*/
|
||||
export const clients = {
|
||||
npm: {
|
||||
check: () => commandExists('npm'),
|
||||
publish: (dir: string, registry: string, token: string) =>
|
||||
runCommand(['npm', 'publish', '--registry', registry], {
|
||||
cwd: dir,
|
||||
env: { NPM_TOKEN: token, npm_config__authToken: token },
|
||||
}),
|
||||
install: (pkg: string, registry: string, dir: string) =>
|
||||
runCommand(['npm', 'install', pkg, '--registry', registry], { cwd: dir }),
|
||||
unpublish: (pkg: string, registry: string, token: string) =>
|
||||
runCommand(['npm', 'unpublish', pkg, '--registry', registry, '--force'], {
|
||||
env: { NPM_TOKEN: token, npm_config__authToken: token },
|
||||
}),
|
||||
pack: (dir: string) => runCommand(['npm', 'pack'], { cwd: dir }),
|
||||
},
|
||||
|
||||
docker: {
|
||||
check: () => commandExists('docker'),
|
||||
build: (dockerfile: string, tag: string, context: string) =>
|
||||
runCommand(['docker', 'build', '-f', dockerfile, '-t', tag, context]),
|
||||
push: (image: string) => runCommand(['docker', 'push', image]),
|
||||
pull: (image: string) => runCommand(['docker', 'pull', image]),
|
||||
rmi: (image: string, force = false) =>
|
||||
runCommand(['docker', 'rmi', ...(force ? ['-f'] : []), image]),
|
||||
login: (registry: string, username: string, password: string) =>
|
||||
runCommand(['docker', 'login', registry, '-u', username, '--password-stdin'], {
|
||||
stdin: password,
|
||||
}),
|
||||
tag: (source: string, target: string) => runCommand(['docker', 'tag', source, target]),
|
||||
},
|
||||
|
||||
cargo: {
|
||||
check: () => commandExists('cargo'),
|
||||
package: (dir: string) => runCommand(['cargo', 'package', '--allow-dirty'], { cwd: dir }),
|
||||
publish: (dir: string, registry: string, token: string) =>
|
||||
runCommand(
|
||||
['cargo', 'publish', '--registry', 'stack-test', '--token', token, '--allow-dirty'],
|
||||
{ cwd: dir }
|
||||
),
|
||||
yank: (crate: string, version: string, token: string) =>
|
||||
runCommand([
|
||||
'cargo',
|
||||
'yank',
|
||||
crate,
|
||||
'--version',
|
||||
version,
|
||||
'--registry',
|
||||
'stack-test',
|
||||
'--token',
|
||||
token,
|
||||
]),
|
||||
},
|
||||
|
||||
pip: {
|
||||
check: () => commandExists('pip'),
|
||||
build: (dir: string) => runCommand(['python', '-m', 'build', dir]),
|
||||
upload: (dist: string, repository: string, token: string) =>
|
||||
runCommand([
|
||||
'python',
|
||||
'-m',
|
||||
'twine',
|
||||
'upload',
|
||||
'--repository-url',
|
||||
repository,
|
||||
'-u',
|
||||
'__token__',
|
||||
'-p',
|
||||
token,
|
||||
`${dist}/*`,
|
||||
]),
|
||||
install: (pkg: string, indexUrl: string) =>
|
||||
runCommand(['pip', 'install', pkg, '--index-url', indexUrl]),
|
||||
},
|
||||
|
||||
composer: {
|
||||
check: () => commandExists('composer'),
|
||||
install: (pkg: string, repository: string, dir: string) =>
|
||||
runCommand(
|
||||
[
|
||||
'composer',
|
||||
'require',
|
||||
pkg,
|
||||
'--repository',
|
||||
JSON.stringify({ type: 'composer', url: repository }),
|
||||
],
|
||||
{ cwd: dir }
|
||||
),
|
||||
},
|
||||
|
||||
gem: {
|
||||
check: () => commandExists('gem'),
|
||||
build: (gemspec: string, dir: string) => runCommand(['gem', 'build', gemspec], { cwd: dir }),
|
||||
push: (gemFile: string, host: string, key: string) =>
|
||||
runCommand(['gem', 'push', gemFile, '--host', host, '--key', key]),
|
||||
install: (gemName: string, source: string) =>
|
||||
runCommand(['gem', 'install', gemName, '--source', source]),
|
||||
yank: (gemName: string, version: string, host: string, key: string) =>
|
||||
runCommand(['gem', 'yank', gemName, '-v', version, '--host', host, '--key', key]),
|
||||
},
|
||||
|
||||
maven: {
|
||||
check: () => commandExists('mvn'),
|
||||
deploy: (dir: string, repositoryUrl: string, username: string, password: string) =>
|
||||
runCommand(
|
||||
[
|
||||
'mvn',
|
||||
'deploy',
|
||||
`-DaltDeploymentRepository=stack-test::default::${repositoryUrl}`,
|
||||
`-Dusername=${username}`,
|
||||
`-Dpassword=${password}`,
|
||||
],
|
||||
{ cwd: dir }
|
||||
),
|
||||
package: (dir: string) => runCommand(['mvn', 'package', '-DskipTests'], { cwd: dir }),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Skip test if command is not available
|
||||
*/
|
||||
export async function skipIfMissing(cmd: string): Promise<boolean> {
|
||||
const exists = await commandExists(cmd);
|
||||
if (!exists) {
|
||||
console.warn(`[Skip] ${cmd} not available`);
|
||||
}
|
||||
return !exists;
|
||||
}
|
||||
Reference in New Issue
Block a user