test: add unit tests for BaseRemoteDataServices functionality and OtelAdapter instrumentation
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import type { BaseEntity } from './types';
|
||||
import { CommonRemoteDataServices } from './common-remote.data-services';
|
||||
|
||||
// ─── Mock AxiosInstance ─────────────────────────────────────────
|
||||
|
||||
function createMockHttpClient(): AxiosInstance {
|
||||
return {
|
||||
request: vi.fn().mockResolvedValue({
|
||||
data: [{ id: '1', name: 'Test' }],
|
||||
status: 200,
|
||||
}),
|
||||
// Satisfy the AxiosInstance shape (unused properties)
|
||||
defaults: {} as AxiosInstance['defaults'],
|
||||
interceptors: {
|
||||
request: { use: vi.fn(), eject: vi.fn(), clear: vi.fn() },
|
||||
response: { use: vi.fn(), eject: vi.fn(), clear: vi.fn() },
|
||||
},
|
||||
getUri: vi.fn(),
|
||||
get: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
head: vi.fn(),
|
||||
options: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
postForm: vi.fn(),
|
||||
putForm: vi.fn(),
|
||||
patchForm: vi.fn(),
|
||||
} as unknown as AxiosInstance;
|
||||
}
|
||||
|
||||
// ─── Test Entity ────────────────────────────────────────────────
|
||||
|
||||
interface TestEntity extends BaseEntity {
|
||||
bookingCode: string;
|
||||
customerName: string;
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────
|
||||
|
||||
describe('BaseRemoteDataServices (via CommonRemoteDataServices)', () => {
|
||||
let mockClient: AxiosInstance;
|
||||
let services: CommonRemoteDataServices<TestEntity>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createMockHttpClient();
|
||||
services = new CommonRemoteDataServices<TestEntity>(mockClient, {
|
||||
apiUrl: '/bookings',
|
||||
moduleKey: 'BOOKING',
|
||||
});
|
||||
});
|
||||
|
||||
// ── URL Interpolation ─────────────────────────────────────────
|
||||
|
||||
describe('URL interpolation', () => {
|
||||
it('getMany() uses the base URL without params', async () => {
|
||||
await services.getMany();
|
||||
|
||||
expect(mockClient.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: '/bookings',
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('getOne() replaces :id in the URL template', async () => {
|
||||
await services.getOne('123');
|
||||
|
||||
expect(mockClient.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: '/bookings/123',
|
||||
method: 'GET',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('getOne() encodes special characters in ID', async () => {
|
||||
await services.getOne('hello world');
|
||||
|
||||
expect(mockClient.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: '/bookings/hello%20world',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('activate() resolves to /:id/active', async () => {
|
||||
await services.activate('42');
|
||||
|
||||
expect(mockClient.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: '/bookings/42/active',
|
||||
method: 'PATCH',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('confirmProcessTransaction() resolves to /:id/confirm-data', async () => {
|
||||
await services.confirmProcessTransaction('99');
|
||||
|
||||
expect(mockClient.request).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: '/bookings/99/confirm-data',
|
||||
method: 'PATCH',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Header Injection ──────────────────────────────────────────
|
||||
|
||||
describe('header injection', () => {
|
||||
it('injects ex-module-key from moduleKey config', async () => {
|
||||
await services.getMany();
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-key']).toBe('BOOKING');
|
||||
});
|
||||
|
||||
it('injects ex-module-action from the descriptor action', async () => {
|
||||
await services.getMany(); // VIEW action
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-action']).toBe('VIEW');
|
||||
});
|
||||
|
||||
it('injects CREATE action for create()', async () => {
|
||||
await services.create({ bookingCode: 'BK001', customerName: 'Test' } as Partial<TestEntity>);
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-action']).toBe('CREATE');
|
||||
});
|
||||
|
||||
it('injects EDIT action for edit()', async () => {
|
||||
await services.edit('42', { customerName: 'Updated' } as Partial<TestEntity>);
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-action']).toBe('EDIT');
|
||||
});
|
||||
|
||||
it('injects DELETE action for delete()', async () => {
|
||||
await services.delete('42');
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-action']).toBe('DELETE');
|
||||
});
|
||||
|
||||
it('omits ex-module-key when moduleKey is not configured', async () => {
|
||||
const noKeyServices = new CommonRemoteDataServices<TestEntity>(mockClient, {
|
||||
apiUrl: '/items',
|
||||
});
|
||||
|
||||
await noKeyServices.getMany();
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-key']).toBeUndefined();
|
||||
// But action is always present
|
||||
expect(requestArg.headers['ex-module-action']).toBe('VIEW');
|
||||
});
|
||||
|
||||
it('preserves caller-provided headers alongside injected ones', async () => {
|
||||
await services.getMany({
|
||||
headers: { 'X-Custom-Header': 'custom-value' },
|
||||
});
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-key']).toBe('BOOKING');
|
||||
expect(requestArg.headers['ex-module-action']).toBe('VIEW');
|
||||
expect(requestArg.headers['X-Custom-Header']).toBe('custom-value');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Telemetry Context ─────────────────────────────────────────
|
||||
|
||||
describe('telemetry context passthrough', () => {
|
||||
it('passes telemetryContext from getMany() config to the Axios request', async () => {
|
||||
const telemetryContext = {
|
||||
customSpanName: 'booking.list.fetch',
|
||||
tags: { region: 'asia' },
|
||||
pushEventOnSuccess: 'booking_list_loaded',
|
||||
};
|
||||
|
||||
await services.getMany({ telemetryContext } as AxiosRequestConfig);
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.telemetryContext).toEqual(telemetryContext);
|
||||
});
|
||||
|
||||
it('passes telemetryContext via customRequest()', async () => {
|
||||
const telemetryContext = {
|
||||
customSpanName: 'custom.tax.calculate',
|
||||
tags: { business: 'tax' },
|
||||
};
|
||||
|
||||
await services.customRequest({
|
||||
url: '/bookings/42/calculate-tax',
|
||||
method: 'POST',
|
||||
data: { items: [] },
|
||||
telemetryContext,
|
||||
});
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.telemetryContext).toEqual(telemetryContext);
|
||||
});
|
||||
|
||||
it('customRequest() still injects ex-module-key header', async () => {
|
||||
await services.customRequest({
|
||||
url: '/bookings/42/calculate-tax',
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.headers['ex-module-key']).toBe('BOOKING');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Response Shape ────────────────────────────────────────────
|
||||
|
||||
describe('response shape', () => {
|
||||
it('returns { data, status } from the Axios response', async () => {
|
||||
(mockClient.request as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
data: { id: '42', bookingCode: 'BK042', customerName: 'Alice' },
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const result = await services.getOne<TestEntity>('42');
|
||||
|
||||
expect(result).toEqual({
|
||||
data: { id: '42', bookingCode: 'BK042', customerName: 'Alice' },
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it('create() passes data in the Axios request config', async () => {
|
||||
const newBooking: Partial<TestEntity> = {
|
||||
bookingCode: 'BK001',
|
||||
customerName: 'Bob',
|
||||
};
|
||||
|
||||
await services.create(newBooking);
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.method).toBe('POST');
|
||||
expect(requestArg.url).toBe('/bookings');
|
||||
expect(requestArg.data).toEqual(newBooking);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Batch Operations ──────────────────────────────────────────
|
||||
|
||||
describe('batch operations', () => {
|
||||
it('batchDelete() sends ids in data payload', async () => {
|
||||
await services.batchDelete(['1', '2', '3']);
|
||||
|
||||
const requestArg = (mockClient.request as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
||||
expect(requestArg.url).toBe('/bookings/batch-delete');
|
||||
expect(requestArg.data).toEqual({ ids: ['1', '2', '3'] });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user