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:
2025-11-28 15:27:04 +00:00
parent 61324ba195
commit 44e92d48f2
50 changed files with 4403 additions and 108 deletions

141
test/helpers/auth.helper.ts Normal file
View 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
View 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;
}

View 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
View 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
View 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';

View 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;
}

View 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;
}