feat(testing): create @bytelyst/testing shared package with 10 tests

Exports:
- createCosmosMocks(): full Cosmos DB mock factory for vitest
- TEST_USERS, TEST_JWT_SECRET, createTestTokenPayload(): auth fixtures
- injectGet/Post/Patch/Delete(): Fastify inject wrappers
- expectHealthOk(): health endpoint assertion helper
This commit is contained in:
saravanakumardb1 2026-02-12 22:59:28 -08:00
parent 428e973548
commit 7ffc60c490
8 changed files with 400 additions and 0 deletions

View File

@ -0,0 +1,35 @@
{
"name": "@bytelyst/testing",
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"test": "vitest run"
},
"devDependencies": {
"@bytelyst/fastify-core": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@fastify/cors": "^10.0.2",
"fastify": "^5.2.1"
},
"peerDependencies": {
"vitest": ">=3.0.0",
"fastify": ">=5.0.0"
},
"peerDependenciesMeta": {
"fastify": {
"optional": true
}
}
}

View File

@ -0,0 +1,100 @@
import { describe, expect, it } from 'vitest';
import {
createCosmosMocks,
TEST_JWT_SECRET,
TEST_USERS,
createTestTokenPayload,
injectGet,
expectHealthOk,
} from '../index.js';
import { createServiceApp } from '@bytelyst/fastify-core';
describe('createCosmosMocks', () => {
it('returns all mock objects', () => {
const mocks = createCosmosMocks();
expect(mocks.mockContainer).toBeDefined();
expect(mocks.mockContainer.id).toBe('test-container');
expect(mocks.mockDatabase).toBeDefined();
expect(mocks.mockDatabases).toBeDefined();
expect(mocks.MockCosmosClient).toBeDefined();
expect(typeof mocks.resetMocks).toBe('function');
});
it('MockCosmosClient returns database', () => {
const { MockCosmosClient, mockDatabase } = createCosmosMocks();
const client = new MockCosmosClient('endpoint', 'key');
expect(client.database('test-db')).toBe(mockDatabase);
});
it('mockContainer.items has CRUD methods', () => {
const { mockContainer } = createCosmosMocks();
expect(typeof mockContainer.items.create).toBe('function');
expect(typeof mockContainer.items.query).toBe('function');
expect(typeof mockContainer.items.upsert).toBe('function');
expect(typeof mockContainer.item).toBe('function');
});
it('resetMocks clears all mock state', () => {
const { mockContainer, resetMocks } = createCosmosMocks();
mockContainer.items.create({ id: '1' });
expect(mockContainer.items.create).toHaveBeenCalledOnce();
resetMocks();
expect(mockContainer.items.create).not.toHaveBeenCalled();
});
});
describe('auth fixtures', () => {
it('TEST_JWT_SECRET is a non-empty string', () => {
expect(TEST_JWT_SECRET).toBeTruthy();
expect(TEST_JWT_SECRET.length).toBeGreaterThanOrEqual(16);
});
it('TEST_USERS has admin, viewer, and user', () => {
expect(TEST_USERS.admin.email).toBe('admin@test.com');
expect(TEST_USERS.viewer.role).toBe('viewer');
expect(TEST_USERS.user.productId).toBe('lysnrai');
});
it('createTestTokenPayload returns valid shape', () => {
const payload = createTestTokenPayload('admin');
expect(payload.sub).toBe('user-admin-001');
expect(payload.email).toBe('admin@test.com');
expect(payload.role).toBe('super_admin');
expect(payload.iss).toBe('test');
expect(payload.exp).toBeGreaterThan(payload.iat);
});
it('createTestTokenPayload defaults to admin', () => {
const payload = createTestTokenPayload();
expect(payload.sub).toBe('user-admin-001');
});
});
describe('fastify helpers', () => {
it('injectGet + expectHealthOk work with a real fastify-core app', async () => {
const app = await createServiceApp({
name: 'test-svc',
version: '1.0.0',
logger: false,
});
const res = await injectGet(app, '/health');
expect(res.statusCode).toBe(200);
expectHealthOk(res, 'test-svc');
await app.close();
});
it('expectHealthOk throws on wrong service name', async () => {
const app = await createServiceApp({
name: 'real-svc',
version: '1.0.0',
logger: false,
});
const res = await injectGet(app, '/health');
expect(() => expectHealthOk(res, 'wrong-name')).toThrow('Expected service "wrong-name"');
await app.close();
});
});

View File

@ -0,0 +1,55 @@
/**
* Auth test fixtures JWT tokens, user payloads, and password helpers.
*
* Usage:
* ```ts
* import { TEST_USERS, TEST_JWT_SECRET, createTestToken } from '@bytelyst/testing';
* ```
*/
/** Test JWT secret — NEVER use in production */
export const TEST_JWT_SECRET = 'test-jwt-secret-32-chars-long!!';
/** Pre-built test user payloads */
export const TEST_USERS = {
admin: {
id: 'user-admin-001',
email: 'admin@test.com',
name: 'Test Admin',
role: 'super_admin',
productId: 'lysnrai',
},
viewer: {
id: 'user-viewer-001',
email: 'viewer@test.com',
name: 'Test Viewer',
role: 'viewer',
productId: 'lysnrai',
},
user: {
id: 'user-basic-001',
email: 'user@test.com',
name: 'Test User',
role: 'user',
productId: 'lysnrai',
},
} as const;
export type TestUserKey = keyof typeof TEST_USERS;
/**
* Create a minimal JWT-like token payload for testing (not cryptographically signed).
* For actual JWT creation, use `@bytelyst/auth` createJwtUtils().
*/
export function createTestTokenPayload(userKey: TestUserKey = 'admin') {
const user = TEST_USERS[userKey];
return {
sub: user.id,
email: user.email,
role: user.role,
productId: user.productId,
iss: 'test',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
};
}

View File

@ -0,0 +1,87 @@
/**
* Shared Cosmos DB mock factories for Vitest.
*
* Usage:
* ```ts
* import { createCosmosMocks } from '@bytelyst/testing';
* const { mockContainer, mockDatabase, MockCosmosClient, resetMocks } = createCosmosMocks();
* vi.mock('@azure/cosmos', () => ({ CosmosClient: MockCosmosClient }));
* ```
*/
import { vi } from 'vitest';
export interface MockItem {
id: string;
[key: string]: unknown;
}
export interface CosmosMocks {
mockContainer: {
id: string;
items: {
create: ReturnType<typeof vi.fn>;
query: ReturnType<typeof vi.fn>;
upsert: ReturnType<typeof vi.fn>;
};
item: ReturnType<typeof vi.fn>;
};
mockDatabase: {
container: ReturnType<typeof vi.fn>;
containers: { createIfNotExists: ReturnType<typeof vi.fn> };
};
mockDatabases: {
createIfNotExists: ReturnType<typeof vi.fn>;
};
MockCosmosClient: ReturnType<typeof vi.fn>;
resetMocks: () => void;
}
/**
* Create a full set of Cosmos DB mocks for unit testing.
*
* Returns mock objects for container, database, and the CosmosClient constructor,
* plus a `resetMocks()` function to clear all mock state between tests.
*/
export function createCosmosMocks(): CosmosMocks {
const mockItem = {
read: vi.fn().mockResolvedValue({ resource: undefined }),
replace: vi.fn().mockResolvedValue({ resource: undefined }),
delete: vi.fn().mockResolvedValue({}),
patch: vi.fn().mockResolvedValue({ resource: undefined }),
};
const mockContainer = {
id: 'test-container',
items: {
create: vi.fn().mockResolvedValue({ resource: {} }),
query: vi.fn().mockReturnValue({
fetchAll: vi.fn().mockResolvedValue({ resources: [] }),
}),
upsert: vi.fn().mockResolvedValue({ resource: {} }),
},
item: vi.fn().mockReturnValue(mockItem),
};
const mockDatabase = {
container: vi.fn().mockReturnValue(mockContainer),
containers: {
createIfNotExists: vi.fn().mockResolvedValue({ container: mockContainer }),
},
};
const mockDatabases = {
createIfNotExists: vi.fn().mockResolvedValue({ database: mockDatabase }),
};
const MockCosmosClient = vi.fn().mockImplementation(() => ({
database: vi.fn().mockReturnValue(mockDatabase),
databases: mockDatabases,
}));
function resetMocks() {
vi.clearAllMocks();
}
return { mockContainer, mockDatabase, mockDatabases, MockCosmosClient, resetMocks };
}

View File

@ -0,0 +1,80 @@
/**
* Fastify test helpers inject wrappers and assertion utilities.
*
* Usage:
* ```ts
* import { injectGet, injectPost, expectHealthOk } from '@bytelyst/testing';
* const res = await injectGet(app, '/health');
* expectHealthOk(res, 'platform-service');
* ```
*/
import type { FastifyInstance } from 'fastify';
interface InjectResult {
statusCode: number;
payload: string;
headers: Record<string, string | string[] | undefined>;
json: () => unknown;
}
/** Inject a GET request into a Fastify instance */
export async function injectGet(
app: FastifyInstance,
url: string,
headers?: Record<string, string>
): Promise<InjectResult> {
return app.inject({ method: 'GET', url, headers }) as unknown as Promise<InjectResult>;
}
/** Inject a POST request with JSON body into a Fastify instance */
export async function injectPost(
app: FastifyInstance,
url: string,
body: unknown,
headers?: Record<string, string>
): Promise<InjectResult> {
return app.inject({
method: 'POST',
url,
payload: body as string,
headers: { 'content-type': 'application/json', ...headers },
}) as unknown as Promise<InjectResult>;
}
/** Inject a PATCH request with JSON body into a Fastify instance */
export async function injectPatch(
app: FastifyInstance,
url: string,
body: unknown,
headers?: Record<string, string>
): Promise<InjectResult> {
return app.inject({
method: 'PATCH',
url,
payload: body as string,
headers: { 'content-type': 'application/json', ...headers },
}) as unknown as Promise<InjectResult>;
}
/** Inject a DELETE request into a Fastify instance */
export async function injectDelete(
app: FastifyInstance,
url: string,
headers?: Record<string, string>
): Promise<InjectResult> {
return app.inject({ method: 'DELETE', url, headers }) as unknown as Promise<InjectResult>;
}
/** Assert a health check response has the correct shape */
export function expectHealthOk(res: InjectResult, serviceName: string): void {
const body = JSON.parse(res.payload);
if (res.statusCode !== 200) throw new Error(`Expected 200, got ${res.statusCode}`);
if (body.status !== 'ok') throw new Error(`Expected status "ok", got "${body.status}"`);
if (body.service !== serviceName)
throw new Error(`Expected service "${serviceName}", got "${body.service}"`);
if (!body.timestamp) throw new Error('Missing timestamp in health response');
if (!body.requestId) throw new Error('Missing requestId in health response');
}
export type { InjectResult };

View File

@ -0,0 +1,15 @@
export { createCosmosMocks, type CosmosMocks, type MockItem } from './cosmos-mocks.js';
export {
TEST_JWT_SECRET,
TEST_USERS,
createTestTokenPayload,
type TestUserKey,
} from './auth-fixtures.js';
export {
injectGet,
injectPost,
injectPatch,
injectDelete,
expectHealthOk,
type InjectResult,
} from './fastify-helpers.js';

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}

19
pnpm-lock.yaml generated
View File

@ -143,6 +143,25 @@ importers:
specifier: ^19.2.4
version: 19.2.4(react@19.2.4)
packages/testing:
dependencies:
vitest:
specifier: '>=3.0.0'
version: 3.2.4(@types/node@22.19.11)(jsdom@28.0.0)(tsx@4.21.0)(yaml@2.8.2)
devDependencies:
'@bytelyst/errors':
specifier: workspace:*
version: link:../errors
'@bytelyst/fastify-core':
specifier: workspace:*
version: link:../fastify-core
'@fastify/cors':
specifier: ^10.0.2
version: 10.1.0
fastify:
specifier: ^5.2.1
version: 5.7.4
services/billing-service:
dependencies:
'@azure/cosmos':