Skip to content

Commit d075af0

Browse files
committed
test: add comprehensive vitest test suite with 100% coverage
Add vitest with v8 coverage provider and 95% threshold enforcement. 75 tests across 10 test files covering all source modules: HttpError, FetchAdapter, AuthenticatedAdapter, EQP, and all 6 services.
1 parent 0cf6c62 commit d075af0

15 files changed

+1631
-4
lines changed

.github/workflows/lint.and.build.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ jobs:
3535
- name: 🔨 Build
3636
run: yarn build:lib
3737

38+
test:
39+
name: 🧪 Test
40+
runs-on: ubuntu-24.04
41+
steps:
42+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
43+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
44+
with:
45+
node-version: 24
46+
cache: yarn
47+
48+
- name: ⚡ Install dependencies
49+
run: yarn install --frozen-lockfile
50+
51+
- name: 🧪 Test
52+
run: yarn test:coverage
53+
3854
lint:
3955
name: 🔎 Lint
4056
runs-on: ubuntu-24.04

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,28 @@
1212
"license": "MIT",
1313
"scripts": {
1414
"build": "yarn build:lib && yarn build:docs",
15-
"build:lib": "tsc",
15+
"build:lib": "tsc -p tsconfig.build.json",
1616
"build:lib:dev": "tsc -w",
1717
"build:docs": "typedoc --out docs --entryPoints src/index.ts",
18+
"test": "vitest run",
19+
"test:coverage": "vitest run --coverage",
1820
"lint": "eslint src",
1921
"prepack": "yarn build:lib",
2022
"prepare": "husky"
2123
},
2224
"devDependencies": {
2325
"@eslint/js": "^10.0.0",
2426
"@types/node": "^24.0.0",
27+
"@vitest/coverage-v8": "^4.0.18",
2528
"eslint": "^10.0.0",
2629
"eslint-config-prettier": "^10.0.0",
2730
"husky": "^9.1.7",
2831
"lint-staged": "^16.2.7",
2932
"prettier": "^3.0.0",
3033
"typedoc": "^0.28.0",
3134
"typescript": "^5.1.6",
32-
"typescript-eslint": "^8.0.0"
35+
"typescript-eslint": "^8.0.0",
36+
"vitest": "^4.0.18"
3337
},
3438
"dependencies": {
3539
"tslib": "^2.6.0"
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { AuthenticatedAdapter } from '../AuthenticatedAdapter';
3+
import { Adapter } from '../types/adapters';
4+
5+
describe('AuthenticatedAdapter', () => {
6+
let mockBaseAdapter: Adapter;
7+
let adapter: AuthenticatedAdapter;
8+
const credentials = { appId: 'test-app-id', appSecret: 'test-app-secret' };
9+
10+
beforeEach(() => {
11+
mockBaseAdapter = {
12+
setHeader: vi.fn(),
13+
get: vi.fn().mockResolvedValue({}),
14+
post: vi.fn().mockResolvedValue({
15+
mage_id: 'MAG123',
16+
ust: 'test-token-xyz',
17+
expires_in: 360
18+
}),
19+
put: vi.fn().mockResolvedValue({}),
20+
delete: vi.fn().mockResolvedValue({})
21+
};
22+
adapter = new AuthenticatedAdapter(mockBaseAdapter, credentials);
23+
});
24+
25+
describe('authenticate', () => {
26+
it('should POST to /app/session/token with Basic auth', async () => {
27+
await adapter.get('/test');
28+
29+
expect(mockBaseAdapter.post).toHaveBeenCalledWith(
30+
'/app/session/token',
31+
{ grant_type: 'session', expires_in: 360 },
32+
{
33+
auth: {
34+
username: 'test-app-id',
35+
password: 'test-app-secret'
36+
}
37+
}
38+
);
39+
});
40+
});
41+
42+
describe('Bearer token', () => {
43+
it('should pass Bearer token in authorization header', async () => {
44+
await adapter.get('/test');
45+
46+
expect(mockBaseAdapter.get).toHaveBeenCalledWith('/test', { headers: { authorization: 'Bearer test-token-xyz' } });
47+
});
48+
});
49+
50+
describe('|MAGE_ID| replacement', () => {
51+
it('should replace |MAGE_ID| in URLs', async () => {
52+
await adapter.get('/users/|MAGE_ID|/profile');
53+
54+
expect(mockBaseAdapter.get).toHaveBeenCalledWith('/users/MAG123/profile', expect.any(Object));
55+
});
56+
57+
it('should replace |MAGE_ID| for POST requests', async () => {
58+
await adapter.post('/users/|MAGE_ID|', { data: 'test' });
59+
60+
expect(mockBaseAdapter.post).toHaveBeenCalledTimes(2); // 1 auth + 1 actual
61+
const actualCall = (mockBaseAdapter.post as ReturnType<typeof vi.fn>).mock.calls[1];
62+
expect(actualCall[0]).toBe('/users/MAG123');
63+
});
64+
65+
it('should replace |MAGE_ID| for PUT requests', async () => {
66+
await adapter.put('/users/|MAGE_ID|', { data: 'test' });
67+
68+
expect(mockBaseAdapter.put).toHaveBeenCalledWith('/users/MAG123', { data: 'test' }, expect.any(Object));
69+
});
70+
71+
it('should replace |MAGE_ID| for DELETE requests', async () => {
72+
await adapter.delete('/users/|MAGE_ID|/key');
73+
74+
expect(mockBaseAdapter.delete).toHaveBeenCalledWith('/users/MAG123/key', expect.any(Object));
75+
});
76+
});
77+
78+
describe('addHeaders', () => {
79+
it('should merge auth headers with existing config headers', async () => {
80+
await adapter.get('/test', { headers: { 'x-custom': 'value' } });
81+
82+
expect(mockBaseAdapter.get).toHaveBeenCalledWith('/test', {
83+
headers: {
84+
'x-custom': 'value',
85+
authorization: 'Bearer test-token-xyz'
86+
}
87+
});
88+
});
89+
90+
it('should work when config is undefined', async () => {
91+
await adapter.get('/test');
92+
93+
expect(mockBaseAdapter.get).toHaveBeenCalledWith('/test', { headers: { authorization: 'Bearer test-token-xyz' } });
94+
});
95+
});
96+
97+
describe('HTTP method delegation', () => {
98+
it('should delegate get', async () => {
99+
await adapter.get('/path', { params: { a: '1' } });
100+
101+
expect(mockBaseAdapter.get).toHaveBeenCalledWith(
102+
'/path',
103+
expect.objectContaining({
104+
params: { a: '1' },
105+
headers: expect.objectContaining({ authorization: 'Bearer test-token-xyz' })
106+
})
107+
);
108+
});
109+
110+
it('should delegate post', async () => {
111+
await adapter.post('/path', { body: true });
112+
113+
const calls = (mockBaseAdapter.post as ReturnType<typeof vi.fn>).mock.calls;
114+
const actualCall = calls[calls.length - 1];
115+
expect(actualCall[0]).toBe('/path');
116+
expect(actualCall[1]).toEqual({ body: true });
117+
});
118+
119+
it('should delegate put', async () => {
120+
await adapter.put('/path', { body: true });
121+
122+
expect(mockBaseAdapter.put).toHaveBeenCalledWith(
123+
'/path',
124+
{ body: true },
125+
expect.objectContaining({
126+
headers: expect.objectContaining({ authorization: 'Bearer test-token-xyz' })
127+
})
128+
);
129+
});
130+
131+
it('should delegate delete', async () => {
132+
await adapter.delete('/path');
133+
134+
expect(mockBaseAdapter.delete).toHaveBeenCalledWith(
135+
'/path',
136+
expect.objectContaining({
137+
headers: expect.objectContaining({ authorization: 'Bearer test-token-xyz' })
138+
})
139+
);
140+
});
141+
});
142+
143+
describe('getMageId', () => {
144+
it('should return the mage_id from authentication', async () => {
145+
const mageId = await adapter.getMageId();
146+
147+
expect(mageId).toBe('MAG123');
148+
});
149+
});
150+
});

src/__tests__/EQP.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { EQP } from '../index';
3+
import { FileService } from '../services/FileService';
4+
import { UserService } from '../services/UserService';
5+
import { KeyService } from '../services/KeyService';
6+
import { CallbackService } from '../services/CallbackService';
7+
import { ReportService } from '../services/ReportService';
8+
import { PackageService } from '../services/PackageService';
9+
10+
// Mock global fetch to prevent real requests during EQP construction
11+
vi.stubGlobal('fetch', vi.fn());
12+
13+
describe('EQP', () => {
14+
const defaultOptions = {
15+
appId: 'test-app-id',
16+
appSecret: 'test-app-secret'
17+
};
18+
19+
describe('environment URLs', () => {
20+
it('should use production URL by default', () => {
21+
const eqp = new EQP(defaultOptions);
22+
23+
expect(eqp).toBeDefined();
24+
});
25+
26+
it('should use production URL when environment is "production"', () => {
27+
const eqp = new EQP({ ...defaultOptions, environment: 'production' });
28+
29+
expect(eqp).toBeDefined();
30+
});
31+
32+
it('should use sandbox URL when environment is "sandbox"', () => {
33+
const eqp = new EQP({ ...defaultOptions, environment: 'sandbox' });
34+
35+
expect(eqp).toBeDefined();
36+
});
37+
});
38+
39+
describe('custom adapter', () => {
40+
it('should use provided adapter instead of creating FetchAdapter', () => {
41+
const customAdapter = {
42+
setHeader: vi.fn(),
43+
get: vi.fn(),
44+
post: vi.fn(),
45+
put: vi.fn(),
46+
delete: vi.fn()
47+
};
48+
49+
const eqp = new EQP({ ...defaultOptions, adapter: customAdapter });
50+
51+
expect(eqp).toBeDefined();
52+
});
53+
});
54+
55+
describe('services', () => {
56+
let eqp: EQP;
57+
58+
beforeEach(() => {
59+
eqp = new EQP(defaultOptions);
60+
});
61+
62+
it('should initialize fileService', () => {
63+
expect(eqp.fileService).toBeInstanceOf(FileService);
64+
});
65+
66+
it('should initialize userService', () => {
67+
expect(eqp.userService).toBeInstanceOf(UserService);
68+
});
69+
70+
it('should initialize keyService', () => {
71+
expect(eqp.keyService).toBeInstanceOf(KeyService);
72+
});
73+
74+
it('should initialize callbackService', () => {
75+
expect(eqp.callbackService).toBeInstanceOf(CallbackService);
76+
});
77+
78+
it('should initialize reportService', () => {
79+
expect(eqp.reportService).toBeInstanceOf(ReportService);
80+
});
81+
82+
it('should initialize packageService', () => {
83+
expect(eqp.packageService).toBeInstanceOf(PackageService);
84+
});
85+
});
86+
87+
describe('getMageId', () => {
88+
it('should delegate to adapter.getMageId()', async () => {
89+
const customAdapter = {
90+
setHeader: vi.fn(),
91+
get: vi.fn(),
92+
post: vi.fn().mockResolvedValue({ mage_id: 'MAG456', ust: 'token', expires_in: 360 }),
93+
put: vi.fn(),
94+
delete: vi.fn()
95+
};
96+
97+
const eqp = new EQP({ ...defaultOptions, adapter: customAdapter });
98+
const mageId = await eqp.getMageId();
99+
100+
expect(mageId).toBe('MAG456');
101+
});
102+
});
103+
});

0 commit comments

Comments
 (0)