Skip to content

Commit 62552c9

Browse files
committed
feat: 70+ coverage
1 parent 2c60795 commit 62552c9

34 files changed

Lines changed: 3709 additions & 25 deletions

package-lock.json

Lines changed: 1028 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"husky": "^9.1.7",
8181
"lint-staged": "^16.2.6",
8282
"prettier": "^3.8.1",
83+
"sqlite3": "^6.0.1",
8384
"supertest": "^7.1.4",
8485
"ts-node": "^10.9.2",
8586
"tsx": "^4.21.0",

src/controllers/magicLinks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ export async function requestMagicLink(req: Request, res: Response) {
4949

5050
const { ip_hash, user_agent_hash } = hashDeviceFingerprint(req.ip, req.headers['user-agent']);
5151

52+
if (!ip_hash || !user_agent_hash) {
53+
logger.error('Could not identify devive metadata to send a magic link');
54+
return res.status(400).json({ error: 'Invalid device data' });
55+
}
5256
// Expire all previous links
5357
await MagicLinkToken.update(
5458
{ expires_at: new Date() },

src/middleware/attachAuthMiddleware.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { verifyBearerAuth } from './verifyBearerAuth.js';
77
import { verifyCookieAuth } from './verifyCookieAuth.js';
88

99
export function attachAuthMiddleware(cookieType: CookieType = 'access') {
10+
console.log(process.env.AUTH_MODE);
1011
const mode = (process.env.AUTH_MODE || 'web').toLowerCase();
1112
return mode === 'server' ? verifyBearerAuth : verifyCookieAuth(cookieType);
1213
}

src/schemas/magicLink.schema.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function validateRedirectUrl(
7272

7373
const isAllowed = allowedOrigins.some((origin) => url.origin === origin);
7474

75-
if (!isAllowed) {
75+
if (!isAllowed || !url) {
7676
return null;
7777
}
7878

tests/factories/sessionFactory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { vi } from 'vitest';
2+
13
export function buildSession(overrides: any = {}) {
24
return {
35
id: 'session-1',
@@ -8,6 +10,7 @@ export function buildSession(overrides: any = {}) {
810
lastUsedAt: new Date().toDateString(),
911
expiresAt: new Date(Date.now() + 100000).toDateString(),
1012
revokedAt: null,
13+
save: vi.fn(),
1114
...overrides,
1215
};
1316
}

tests/factories/userFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { vi } from 'vitest';
22

3-
let idCounter = 1;
43
export let testGuid = 'c6e39f68-a09d-49dd-86b4-eab2c1e5de52';
54

65
export function buildUser(overrides: Partial<any> = {}) {
@@ -11,6 +10,8 @@ export function buildUser(overrides: Partial<any> = {}) {
1110
roles: ['user'],
1211
challenge: 'challenge',
1312
createdAt: Date.now(),
13+
emailVerified: true,
14+
phoneVerified: true,
1415
toJSON: vi.fn(() => ({ id: 'user-1' })),
1516
update: vi.fn(),
1617
destroy: vi.fn(),
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { vi } from 'vitest';
2+
3+
vi.mock('../../../src/models/systemConfig', () => ({
4+
SystemConfig: {
5+
findByPk: vi.fn(),
6+
create: vi.fn(),
7+
},
8+
}));
9+
10+
vi.mock('../../../src/utils/parseEnvConfigs', () => ({
11+
parseSystemConfigEnvValue: vi.fn(),
12+
}));
13+
14+
vi.mock('../../../src/config/systemConfig.envMap', () => ({
15+
SYSTEM_CONFIG_ENV_MAP: {
16+
app_name: 'APP_NAME',
17+
rate_limit: 'RATE_LIMIT',
18+
},
19+
}));
20+
21+
vi.mock('../../../src/schemas/systemConfig.schema', () => ({
22+
SystemConfigSchema: {
23+
safeParse: vi.fn(),
24+
},
25+
}));
26+
27+
function resetEnv() {
28+
delete process.env.APP_NAME;
29+
delete process.env.RATE_LIMIT;
30+
}
31+
32+
import { describe, it, expect, beforeEach } from 'vitest';
33+
34+
describe('bootstrapSystemConfig', () => {
35+
beforeEach(() => {
36+
vi.resetModules();
37+
vi.clearAllMocks();
38+
resetEnv();
39+
});
40+
41+
it('uses existing config from DB', async () => {
42+
const { SystemConfig } = await import('../../../src/models/systemConfig');
43+
const { SystemConfigSchema } = await import('../../../src/schemas/systemConfig.schema');
44+
45+
(SystemConfig.findByPk as any).mockResolvedValue({
46+
value: 'existing',
47+
});
48+
49+
(SystemConfigSchema.safeParse as any).mockReturnValue({
50+
success: true,
51+
data: { app_name: 'existing', rate_limit: 'existing' },
52+
});
53+
54+
const { bootstrapSystemConfig } = await import('../../../src/config/bootstrapSystemConfig');
55+
56+
const result = await bootstrapSystemConfig();
57+
58+
expect(result).toBeDefined();
59+
expect(SystemConfig.create).not.toHaveBeenCalled();
60+
});
61+
62+
it('creates config from env when missing', async () => {
63+
const { SystemConfig } = await import('../../../src/models/systemConfig');
64+
const { parseSystemConfigEnvValue } = await import('../../../src/utils/parseEnvConfigs');
65+
const { SystemConfigSchema } = await import('../../../src/schemas/systemConfig.schema');
66+
67+
(SystemConfig.findByPk as any).mockResolvedValue(null);
68+
69+
process.env.APP_NAME = 'TestApp';
70+
process.env.RATE_LIMIT = '100';
71+
72+
(parseSystemConfigEnvValue as any).mockReturnValue('parsed');
73+
74+
(SystemConfigSchema.safeParse as any).mockReturnValue({
75+
success: true,
76+
data: { app_name: 'parsed', rate_limit: 'parsed' },
77+
});
78+
79+
const { bootstrapSystemConfig } = await import('../../../src/config/bootstrapSystemConfig');
80+
81+
const result = await bootstrapSystemConfig();
82+
83+
expect(SystemConfig.create).toHaveBeenCalled();
84+
expect(result).toBeDefined();
85+
});
86+
87+
it('throws when env missing', async () => {
88+
const { SystemConfig } = await import('../../../src/models/systemConfig');
89+
90+
(SystemConfig.findByPk as any).mockResolvedValue(null);
91+
92+
const { bootstrapSystemConfig } = await import('../../../src/config/bootstrapSystemConfig');
93+
94+
await expect(bootstrapSystemConfig()).rejects.toThrow('Missing required system config');
95+
});
96+
97+
it('throws when schema invalid', async () => {
98+
const { SystemConfig } = await import('../../../src/models/systemConfig');
99+
const { parseSystemConfigEnvValue } = await import('../../../src/utils/parseEnvConfigs');
100+
const { SystemConfigSchema } = await import('../../../src/schemas/systemConfig.schema');
101+
102+
(SystemConfig.findByPk as any).mockResolvedValue(null);
103+
104+
process.env.APP_NAME = 'TestApp';
105+
process.env.RATE_LIMIT = '100';
106+
107+
(parseSystemConfigEnvValue as any).mockReturnValue('parsed');
108+
109+
(SystemConfigSchema.safeParse as any).mockReturnValue({
110+
success: false,
111+
error: { toString: () => 'invalid schema' },
112+
});
113+
114+
const { bootstrapSystemConfig } = await import('../../../src/config/bootstrapSystemConfig');
115+
116+
await expect(bootstrapSystemConfig()).rejects.toThrow('Invalid system configuration');
117+
});
118+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
3+
vi.unmock('../../../src/config/getSystemConfig');
4+
5+
describe('getSystemConfig', () => {
6+
beforeEach(() => {
7+
vi.resetModules();
8+
vi.clearAllMocks();
9+
});
10+
11+
it('fetches config from DB when cache empty', async () => {
12+
const { SystemConfig } = await import('../../../src/models/systemConfig');
13+
14+
(SystemConfig.findAll as any).mockResolvedValue([{ key: 'app_name', value: 'TestApp' }]);
15+
16+
const { getSystemConfig } = await import('../../../src/config/getSystemConfig');
17+
18+
const result = await getSystemConfig();
19+
20+
expect(SystemConfig.findAll).toHaveBeenCalled();
21+
expect(result).toEqual({ app_name: 'TestApp' });
22+
});
23+
24+
it('returns cached config when within TTL', async () => {
25+
const { SystemConfig } = await import('../../../src/models/systemConfig');
26+
27+
(SystemConfig.findAll as any).mockResolvedValue([{ key: 'app_name', value: 'TestApp' }]);
28+
29+
const { getSystemConfig } = await import('../../../src/config/getSystemConfig');
30+
31+
const first = await getSystemConfig();
32+
const second = await getSystemConfig();
33+
34+
expect(SystemConfig.findAll).toHaveBeenCalledTimes(1);
35+
expect(second).toEqual(first);
36+
});
37+
38+
it('refreshes cache after TTL expires', async () => {
39+
const { SystemConfig } = await import('../../../src/models/systemConfig');
40+
41+
(SystemConfig.findAll as any)
42+
.mockResolvedValueOnce([{ key: 'app_name', value: 'A' }])
43+
.mockResolvedValueOnce([{ key: 'app_name', value: 'B' }]);
44+
45+
const { getSystemConfig } = await import('../../../src/config/getSystemConfig');
46+
47+
const first = await getSystemConfig();
48+
49+
// simulate time passing
50+
vi.spyOn(Date, 'now')
51+
.mockReturnValueOnce(Date.now() + 1)
52+
.mockReturnValueOnce(Date.now() + 400_000); // > TTL
53+
54+
const second = await getSystemConfig();
55+
56+
expect(second).not.toEqual(first);
57+
expect(SystemConfig.findAll).toHaveBeenCalledTimes(2);
58+
});
59+
60+
it('invalidates cache manually', async () => {
61+
const { SystemConfig } = await import('../../../src/models/systemConfig');
62+
63+
(SystemConfig.findAll as any)
64+
.mockResolvedValueOnce([{ key: 'app_name', value: 'A' }])
65+
.mockResolvedValueOnce([{ key: 'app_name', value: 'B' }]);
66+
67+
const { getSystemConfig, invalidateSystemConfigCache } =
68+
await import('../../../src/config/getSystemConfig');
69+
70+
await getSystemConfig();
71+
72+
invalidateSystemConfigCache();
73+
74+
const result = await getSystemConfig();
75+
76+
expect(result).toEqual({ app_name: 'B' });
77+
expect(SystemConfig.findAll).toHaveBeenCalledTimes(2);
78+
});
79+
});

0 commit comments

Comments
 (0)