@markwharton/pwa-core 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/auth/token.test.js +23 -0
- package/dist/__tests__/client/api.test.js +65 -0
- package/dist/__tests__/server/auth/apiKey.test.d.ts +1 -0
- package/dist/__tests__/server/auth/apiKey.test.js +80 -0
- package/dist/__tests__/server/auth/token.test.d.ts +1 -0
- package/dist/__tests__/server/auth/token.test.js +212 -0
- package/dist/__tests__/server/http/responses.test.d.ts +1 -0
- package/dist/__tests__/server/http/responses.test.js +112 -0
- package/dist/__tests__/server/storage/client.test.d.ts +1 -0
- package/dist/__tests__/server/storage/client.test.js +173 -0
- package/dist/__tests__/server/storage/keys.test.d.ts +1 -0
- package/dist/__tests__/server/storage/keys.test.js +47 -0
- package/dist/__tests__/shared/auth/types.test.d.ts +1 -0
- package/dist/__tests__/shared/auth/types.test.js +77 -0
- package/dist/__tests__/shared/http/status.test.d.ts +1 -0
- package/dist/__tests__/shared/http/status.test.js +27 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +6 -5
- package/dist/server/auth/apiKey.d.ts +44 -0
- package/dist/server/auth/apiKey.js +59 -0
- package/dist/server/auth/index.d.ts +2 -0
- package/dist/server/auth/index.js +15 -0
- package/dist/server/auth/token.d.ts +56 -0
- package/dist/server/auth/token.js +104 -0
- package/dist/server/http/index.d.ts +1 -0
- package/dist/server/http/index.js +12 -0
- package/dist/server/http/responses.d.ts +82 -0
- package/dist/server/http/responses.js +132 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +33 -0
- package/dist/server/storage/client.d.ts +48 -0
- package/dist/server/storage/client.js +107 -0
- package/dist/server/storage/index.d.ts +2 -0
- package/dist/server/storage/index.js +11 -0
- package/dist/server/storage/keys.d.ts +8 -0
- package/dist/server/storage/keys.js +14 -0
- package/dist/shared/auth/index.d.ts +2 -0
- package/dist/shared/auth/index.js +7 -0
- package/dist/shared/auth/types.d.ts +63 -0
- package/dist/shared/auth/types.js +41 -0
- package/dist/shared/http/index.d.ts +3 -0
- package/dist/shared/http/index.js +5 -0
- package/dist/shared/http/status.d.ts +17 -0
- package/dist/shared/http/status.js +19 -0
- package/dist/shared/http/types.d.ts +10 -0
- package/dist/shared/http/types.js +5 -0
- package/dist/shared/index.d.ts +5 -0
- package/dist/shared/index.js +10 -0
- package/package.json +9 -13
|
@@ -134,6 +134,29 @@ const token_1 = require("../../auth/token");
|
|
|
134
134
|
const result = validateToken(wrongToken);
|
|
135
135
|
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
136
136
|
});
|
|
137
|
+
(0, vitest_1.it)('returns error for token with string payload', async () => {
|
|
138
|
+
const { initAuth, validateToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
139
|
+
initAuth(validSecret);
|
|
140
|
+
// jwt.sign with a string payload returns a string from jwt.verify
|
|
141
|
+
const stringPayloadToken = jsonwebtoken_1.default.sign('not-an-object', validSecret);
|
|
142
|
+
const result = validateToken(stringPayloadToken);
|
|
143
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
144
|
+
(0, vitest_1.expect)(result.error).toBe('Invalid token payload');
|
|
145
|
+
});
|
|
146
|
+
(0, vitest_1.it)('returns default error message for non-Error throw', async () => {
|
|
147
|
+
const { initAuth, validateToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
148
|
+
initAuth(validSecret);
|
|
149
|
+
// Mock jwt.verify to throw a non-Error value
|
|
150
|
+
const originalVerify = jsonwebtoken_1.default.verify;
|
|
151
|
+
jsonwebtoken_1.default.verify = () => {
|
|
152
|
+
throw 'string error';
|
|
153
|
+
};
|
|
154
|
+
const result = validateToken('any-token');
|
|
155
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
156
|
+
(0, vitest_1.expect)(result.error).toBe('Token validation failed');
|
|
157
|
+
// Restore original
|
|
158
|
+
jsonwebtoken_1.default.verify = originalVerify;
|
|
159
|
+
});
|
|
137
160
|
});
|
|
138
161
|
(0, vitest_1.describe)('generateToken', () => {
|
|
139
162
|
(0, vitest_1.it)('generates valid JWT', async () => {
|
|
@@ -136,6 +136,19 @@ global.fetch = mockFetch;
|
|
|
136
136
|
message: 'Request failed'
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
|
+
(0, vitest_1.it)('uses default error message when error field is missing', async () => {
|
|
140
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
141
|
+
initApiClient({ getToken: () => 'token' });
|
|
142
|
+
mockFetch.mockResolvedValue({
|
|
143
|
+
ok: false,
|
|
144
|
+
status: 400,
|
|
145
|
+
json: () => Promise.resolve({ message: 'not the error field' })
|
|
146
|
+
});
|
|
147
|
+
await (0, vitest_1.expect)(apiCall('/api/test')).rejects.toMatchObject({
|
|
148
|
+
status: 400,
|
|
149
|
+
message: 'Request failed'
|
|
150
|
+
});
|
|
151
|
+
});
|
|
139
152
|
});
|
|
140
153
|
(0, vitest_1.describe)('HTTP method helpers', () => {
|
|
141
154
|
(0, vitest_1.beforeEach)(async () => {
|
|
@@ -177,6 +190,14 @@ global.fetch = mockFetch;
|
|
|
177
190
|
body: '{"name":"updated"}'
|
|
178
191
|
}));
|
|
179
192
|
});
|
|
193
|
+
(0, vitest_1.it)('apiPut works without body', async () => {
|
|
194
|
+
const { apiPut } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
195
|
+
await apiPut('/api/resource');
|
|
196
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
197
|
+
method: 'PUT',
|
|
198
|
+
body: undefined
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
180
201
|
(0, vitest_1.it)('apiPatch uses PATCH method with body', async () => {
|
|
181
202
|
const { apiPatch } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
182
203
|
await apiPatch('/api/resource', { field: 'value' });
|
|
@@ -185,6 +206,14 @@ global.fetch = mockFetch;
|
|
|
185
206
|
body: '{"field":"value"}'
|
|
186
207
|
}));
|
|
187
208
|
});
|
|
209
|
+
(0, vitest_1.it)('apiPatch works without body', async () => {
|
|
210
|
+
const { apiPatch } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
211
|
+
await apiPatch('/api/resource');
|
|
212
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
213
|
+
method: 'PATCH',
|
|
214
|
+
body: undefined
|
|
215
|
+
}));
|
|
216
|
+
});
|
|
188
217
|
(0, vitest_1.it)('apiDelete uses DELETE method', async () => {
|
|
189
218
|
const { apiDelete } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
190
219
|
await apiDelete('/api/resource');
|
|
@@ -265,5 +294,41 @@ global.fetch = mockFetch;
|
|
|
265
294
|
(0, vitest_1.expect)(result.status).toBe(204);
|
|
266
295
|
(0, vitest_1.expect)(result.data).toBeUndefined();
|
|
267
296
|
});
|
|
297
|
+
(0, vitest_1.it)('handles request without token', async () => {
|
|
298
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
299
|
+
initApiClient({ getToken: () => null });
|
|
300
|
+
mockFetch.mockResolvedValue({
|
|
301
|
+
ok: true,
|
|
302
|
+
status: 200,
|
|
303
|
+
text: () => Promise.resolve('{}')
|
|
304
|
+
});
|
|
305
|
+
await apiCallSafe('/api/public');
|
|
306
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
307
|
+
(0, vitest_1.expect)(headers.Authorization).toBeUndefined();
|
|
308
|
+
});
|
|
309
|
+
(0, vitest_1.it)('uses default error message when error field is missing', async () => {
|
|
310
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
311
|
+
initApiClient({ getToken: () => 'token' });
|
|
312
|
+
mockFetch.mockResolvedValue({
|
|
313
|
+
ok: false,
|
|
314
|
+
status: 400,
|
|
315
|
+
json: () => Promise.resolve({ details: 'not the error field' })
|
|
316
|
+
});
|
|
317
|
+
const result = await apiCallSafe('/api/test');
|
|
318
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
319
|
+
(0, vitest_1.expect)(result.error).toBe('Request failed');
|
|
320
|
+
});
|
|
321
|
+
(0, vitest_1.it)('handles JSON parse error in error response', async () => {
|
|
322
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
323
|
+
initApiClient({ getToken: () => 'token' });
|
|
324
|
+
mockFetch.mockResolvedValue({
|
|
325
|
+
ok: false,
|
|
326
|
+
status: 500,
|
|
327
|
+
json: () => Promise.reject(new Error('Invalid JSON'))
|
|
328
|
+
});
|
|
329
|
+
const result = await apiCallSafe('/api/test');
|
|
330
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
331
|
+
(0, vitest_1.expect)(result.error).toBe('Request failed');
|
|
332
|
+
});
|
|
268
333
|
});
|
|
269
334
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const apiKey_1 = require("../../../server/auth/apiKey");
|
|
5
|
+
(0, vitest_1.describe)('API Key utilities', () => {
|
|
6
|
+
(0, vitest_1.describe)('extractApiKey', () => {
|
|
7
|
+
(0, vitest_1.it)('extracts API key from X-API-Key header', () => {
|
|
8
|
+
const request = {
|
|
9
|
+
headers: {
|
|
10
|
+
get: (name) => name === 'X-API-Key' ? 'test-api-key' : null
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
(0, vitest_1.expect)((0, apiKey_1.extractApiKey)(request)).toBe('test-api-key');
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)('returns null when header is missing', () => {
|
|
16
|
+
const request = {
|
|
17
|
+
headers: {
|
|
18
|
+
get: () => null
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
(0, vitest_1.expect)((0, apiKey_1.extractApiKey)(request)).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.describe)('hashApiKey', () => {
|
|
25
|
+
(0, vitest_1.it)('produces consistent SHA-256 hash', () => {
|
|
26
|
+
const key = 'test-api-key';
|
|
27
|
+
const hash1 = (0, apiKey_1.hashApiKey)(key);
|
|
28
|
+
const hash2 = (0, apiKey_1.hashApiKey)(key);
|
|
29
|
+
(0, vitest_1.expect)(hash1).toBe(hash2);
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)('produces 64-character hex string', () => {
|
|
32
|
+
const hash = (0, apiKey_1.hashApiKey)('any-key');
|
|
33
|
+
(0, vitest_1.expect)(hash).toHaveLength(64);
|
|
34
|
+
(0, vitest_1.expect)(hash).toMatch(/^[a-f0-9]+$/);
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)('produces different hashes for different keys', () => {
|
|
37
|
+
const hash1 = (0, apiKey_1.hashApiKey)('key1');
|
|
38
|
+
const hash2 = (0, apiKey_1.hashApiKey)('key2');
|
|
39
|
+
(0, vitest_1.expect)(hash1).not.toBe(hash2);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.describe)('validateApiKey', () => {
|
|
43
|
+
(0, vitest_1.it)('returns ok for matching key', () => {
|
|
44
|
+
const key = 'valid-api-key';
|
|
45
|
+
const hash = (0, apiKey_1.hashApiKey)(key);
|
|
46
|
+
const result = (0, apiKey_1.validateApiKey)(key, hash);
|
|
47
|
+
(0, vitest_1.expect)(result.ok).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.it)('returns error for mismatched key', () => {
|
|
50
|
+
const hash = (0, apiKey_1.hashApiKey)('correct-key');
|
|
51
|
+
const result = (0, apiKey_1.validateApiKey)('wrong-key', hash);
|
|
52
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
53
|
+
(0, vitest_1.expect)(result.error).toBe('Invalid API key');
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('returns error for empty key', () => {
|
|
56
|
+
const hash = (0, apiKey_1.hashApiKey)('some-key');
|
|
57
|
+
const result = (0, apiKey_1.validateApiKey)('', hash);
|
|
58
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.describe)('generateApiKey', () => {
|
|
62
|
+
(0, vitest_1.it)('generates 64-character hex string', () => {
|
|
63
|
+
const key = (0, apiKey_1.generateApiKey)();
|
|
64
|
+
(0, vitest_1.expect)(key).toHaveLength(64);
|
|
65
|
+
(0, vitest_1.expect)(key).toMatch(/^[a-f0-9]+$/);
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.it)('generates unique keys', () => {
|
|
68
|
+
const key1 = (0, apiKey_1.generateApiKey)();
|
|
69
|
+
const key2 = (0, apiKey_1.generateApiKey)();
|
|
70
|
+
(0, vitest_1.expect)(key1).not.toBe(key2);
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)('generates cryptographically random keys', () => {
|
|
73
|
+
const keys = new Set();
|
|
74
|
+
for (let i = 0; i < 100; i++) {
|
|
75
|
+
keys.add((0, apiKey_1.generateApiKey)());
|
|
76
|
+
}
|
|
77
|
+
(0, vitest_1.expect)(keys.size).toBe(100);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const vitest_1 = require("vitest");
|
|
40
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
41
|
+
const token_1 = require("../../../server/auth/token");
|
|
42
|
+
// Reset module state before each test
|
|
43
|
+
(0, vitest_1.beforeEach)(() => {
|
|
44
|
+
// Re-import to reset module state
|
|
45
|
+
vitest_1.vi.resetModules();
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.describe)('JWT utilities', () => {
|
|
48
|
+
const validSecret = 'a'.repeat(32); // 32 character secret
|
|
49
|
+
(0, vitest_1.describe)('initAuth', () => {
|
|
50
|
+
(0, vitest_1.it)('initializes with valid secret', async () => {
|
|
51
|
+
const { initAuth, getJwtSecret } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
52
|
+
initAuth(validSecret);
|
|
53
|
+
(0, vitest_1.expect)(getJwtSecret()).toBe(validSecret);
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('throws if secret is undefined', async () => {
|
|
56
|
+
const { initAuth } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
57
|
+
(0, vitest_1.expect)(() => initAuth(undefined)).toThrow('JWT_SECRET must be at least 32 characters');
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)('throws if secret is too short', async () => {
|
|
60
|
+
const { initAuth } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
61
|
+
(0, vitest_1.expect)(() => initAuth('short')).toThrow('JWT_SECRET must be at least 32 characters');
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)('accepts custom minimum length', async () => {
|
|
64
|
+
const { initAuth, getJwtSecret } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
65
|
+
const shortSecret = 'a'.repeat(16);
|
|
66
|
+
initAuth(shortSecret, 16);
|
|
67
|
+
(0, vitest_1.expect)(getJwtSecret()).toBe(shortSecret);
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('throws if secret shorter than custom minimum', async () => {
|
|
70
|
+
const { initAuth } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
71
|
+
(0, vitest_1.expect)(() => initAuth('a'.repeat(15), 16)).toThrow('JWT_SECRET must be at least 16 characters');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.describe)('getJwtSecret', () => {
|
|
75
|
+
(0, vitest_1.it)('throws if not initialized', async () => {
|
|
76
|
+
const { getJwtSecret } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
77
|
+
(0, vitest_1.expect)(() => getJwtSecret()).toThrow('Auth not initialized. Call initAuth() first.');
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)('returns secret after initialization', async () => {
|
|
80
|
+
const { initAuth, getJwtSecret } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
81
|
+
initAuth(validSecret);
|
|
82
|
+
(0, vitest_1.expect)(getJwtSecret()).toBe(validSecret);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.describe)('extractToken', () => {
|
|
86
|
+
(0, vitest_1.it)('extracts token from Bearer header', () => {
|
|
87
|
+
(0, vitest_1.expect)((0, token_1.extractToken)('Bearer mytoken123')).toBe('mytoken123');
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.it)('returns null for null header', () => {
|
|
90
|
+
(0, vitest_1.expect)((0, token_1.extractToken)(null)).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)('returns null for empty header', () => {
|
|
93
|
+
(0, vitest_1.expect)((0, token_1.extractToken)('')).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.it)('returns null for non-Bearer header', () => {
|
|
96
|
+
(0, vitest_1.expect)((0, token_1.extractToken)('Basic abc123')).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
(0, vitest_1.it)('returns null for malformed Bearer header', () => {
|
|
99
|
+
(0, vitest_1.expect)((0, token_1.extractToken)('Bearertoken')).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.it)('handles token with spaces', () => {
|
|
102
|
+
(0, vitest_1.expect)((0, token_1.extractToken)('Bearer token with spaces')).toBe('token with spaces');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.describe)('validateToken', () => {
|
|
106
|
+
(0, vitest_1.it)('returns ok result for valid token', async () => {
|
|
107
|
+
const { initAuth, validateToken, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
108
|
+
initAuth(validSecret);
|
|
109
|
+
const payload = { userId: '123' };
|
|
110
|
+
const token = generateToken(payload);
|
|
111
|
+
const result = validateToken(token);
|
|
112
|
+
(0, vitest_1.expect)(result.ok).toBe(true);
|
|
113
|
+
(0, vitest_1.expect)(result.data?.userId).toBe('123');
|
|
114
|
+
});
|
|
115
|
+
(0, vitest_1.it)('returns error for invalid token', async () => {
|
|
116
|
+
const { initAuth, validateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
117
|
+
initAuth(validSecret);
|
|
118
|
+
const result = validateToken('invalid-token');
|
|
119
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
120
|
+
(0, vitest_1.expect)(result.error).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
(0, vitest_1.it)('returns error for expired token', async () => {
|
|
123
|
+
const { initAuth, validateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
124
|
+
initAuth(validSecret);
|
|
125
|
+
const expiredToken = jsonwebtoken_1.default.sign({ userId: '123' }, validSecret, { expiresIn: '-1s' });
|
|
126
|
+
const result = validateToken(expiredToken);
|
|
127
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
128
|
+
(0, vitest_1.expect)(result.error).toContain('expired');
|
|
129
|
+
});
|
|
130
|
+
(0, vitest_1.it)('returns error for token signed with different secret', async () => {
|
|
131
|
+
const { initAuth, validateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
132
|
+
initAuth(validSecret);
|
|
133
|
+
const wrongToken = jsonwebtoken_1.default.sign({ userId: '123' }, 'different-secret-that-is-32-chars');
|
|
134
|
+
const result = validateToken(wrongToken);
|
|
135
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
(0, vitest_1.it)('returns error for token with string payload', async () => {
|
|
138
|
+
const { initAuth, validateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
139
|
+
initAuth(validSecret);
|
|
140
|
+
// jwt.sign with a string payload returns a string from jwt.verify
|
|
141
|
+
const stringPayloadToken = jsonwebtoken_1.default.sign('not-an-object', validSecret);
|
|
142
|
+
const result = validateToken(stringPayloadToken);
|
|
143
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
144
|
+
(0, vitest_1.expect)(result.error).toBe('Invalid token payload');
|
|
145
|
+
});
|
|
146
|
+
(0, vitest_1.it)('returns default error message for non-Error throw', async () => {
|
|
147
|
+
const { initAuth, validateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
148
|
+
initAuth(validSecret);
|
|
149
|
+
// Mock jwt.verify to throw a non-Error value
|
|
150
|
+
const originalVerify = jsonwebtoken_1.default.verify;
|
|
151
|
+
jsonwebtoken_1.default.verify = () => {
|
|
152
|
+
throw 'string error';
|
|
153
|
+
};
|
|
154
|
+
const result = validateToken('any-token');
|
|
155
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
156
|
+
(0, vitest_1.expect)(result.error).toBe('Token validation failed');
|
|
157
|
+
// Restore original
|
|
158
|
+
jsonwebtoken_1.default.verify = originalVerify;
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
(0, vitest_1.describe)('generateToken', () => {
|
|
162
|
+
(0, vitest_1.it)('generates valid JWT', async () => {
|
|
163
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
164
|
+
initAuth(validSecret);
|
|
165
|
+
const token = generateToken({ userId: '123' });
|
|
166
|
+
(0, vitest_1.expect)(token).toBeTruthy();
|
|
167
|
+
(0, vitest_1.expect)(token.split('.')).toHaveLength(3);
|
|
168
|
+
});
|
|
169
|
+
(0, vitest_1.it)('includes payload in token', async () => {
|
|
170
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
171
|
+
initAuth(validSecret);
|
|
172
|
+
const token = generateToken({ userId: '123', role: 'admin' });
|
|
173
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
174
|
+
(0, vitest_1.expect)(decoded.userId).toBe('123');
|
|
175
|
+
(0, vitest_1.expect)(decoded.role).toBe('admin');
|
|
176
|
+
});
|
|
177
|
+
(0, vitest_1.it)('uses default 7d expiration', async () => {
|
|
178
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
179
|
+
initAuth(validSecret);
|
|
180
|
+
const token = generateToken({ userId: '123' });
|
|
181
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
182
|
+
const expiresInSeconds = decoded.exp - decoded.iat;
|
|
183
|
+
(0, vitest_1.expect)(expiresInSeconds).toBe(7 * 24 * 60 * 60); // 7 days
|
|
184
|
+
});
|
|
185
|
+
(0, vitest_1.it)('accepts custom expiration', async () => {
|
|
186
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
187
|
+
initAuth(validSecret);
|
|
188
|
+
const token = generateToken({ userId: '123' }, '1h');
|
|
189
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
190
|
+
const expiresInSeconds = decoded.exp - decoded.iat;
|
|
191
|
+
(0, vitest_1.expect)(expiresInSeconds).toBe(60 * 60); // 1 hour
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
(0, vitest_1.describe)('generateLongLivedToken', () => {
|
|
195
|
+
(0, vitest_1.it)('generates token with 10-year default expiration', async () => {
|
|
196
|
+
const { initAuth, generateLongLivedToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
197
|
+
initAuth(validSecret);
|
|
198
|
+
const token = generateLongLivedToken({ userId: '123' });
|
|
199
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
200
|
+
const expiresInDays = (decoded.exp - decoded.iat) / (24 * 60 * 60);
|
|
201
|
+
(0, vitest_1.expect)(expiresInDays).toBe(3650);
|
|
202
|
+
});
|
|
203
|
+
(0, vitest_1.it)('accepts custom expiration in days', async () => {
|
|
204
|
+
const { initAuth, generateLongLivedToken } = await Promise.resolve().then(() => __importStar(require('../../../server/auth/token')));
|
|
205
|
+
initAuth(validSecret);
|
|
206
|
+
const token = generateLongLivedToken({ userId: '123' }, 365);
|
|
207
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
208
|
+
const expiresInDays = (decoded.exp - decoded.iat) / (24 * 60 * 60);
|
|
209
|
+
(0, vitest_1.expect)(expiresInDays).toBe(365);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const responses_1 = require("../../../server/http/responses");
|
|
5
|
+
const status_1 = require("../../../shared/http/status");
|
|
6
|
+
(0, vitest_1.describe)('HTTP response helpers', () => {
|
|
7
|
+
(0, vitest_1.describe)('badRequestResponse', () => {
|
|
8
|
+
(0, vitest_1.it)('returns 400 status with error message', () => {
|
|
9
|
+
const response = (0, responses_1.badRequestResponse)('Invalid input');
|
|
10
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.BAD_REQUEST);
|
|
11
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'Invalid input' });
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
(0, vitest_1.describe)('unauthorizedResponse', () => {
|
|
15
|
+
(0, vitest_1.it)('returns 401 status with default message', () => {
|
|
16
|
+
const response = (0, responses_1.unauthorizedResponse)();
|
|
17
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.UNAUTHORIZED);
|
|
18
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'Unauthorized' });
|
|
19
|
+
});
|
|
20
|
+
(0, vitest_1.it)('returns 401 status with custom message', () => {
|
|
21
|
+
const response = (0, responses_1.unauthorizedResponse)('Token expired');
|
|
22
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.UNAUTHORIZED);
|
|
23
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'Token expired' });
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.describe)('forbiddenResponse', () => {
|
|
27
|
+
(0, vitest_1.it)('returns 403 status with default message', () => {
|
|
28
|
+
const response = (0, responses_1.forbiddenResponse)();
|
|
29
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.FORBIDDEN);
|
|
30
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'Forbidden' });
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)('returns 403 status with custom message', () => {
|
|
33
|
+
const response = (0, responses_1.forbiddenResponse)('Admin access required');
|
|
34
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.FORBIDDEN);
|
|
35
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'Admin access required' });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.describe)('notFoundResponse', () => {
|
|
39
|
+
(0, vitest_1.it)('returns 404 status with resource name', () => {
|
|
40
|
+
const response = (0, responses_1.notFoundResponse)('User');
|
|
41
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.NOT_FOUND);
|
|
42
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'User not found' });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.describe)('conflictResponse', () => {
|
|
46
|
+
(0, vitest_1.it)('returns 409 status with error message', () => {
|
|
47
|
+
const response = (0, responses_1.conflictResponse)('Resource already exists');
|
|
48
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.CONFLICT);
|
|
49
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'Resource already exists' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.describe)('handleFunctionError', () => {
|
|
53
|
+
(0, vitest_1.it)('logs error and returns 500 response', () => {
|
|
54
|
+
const mockContext = {
|
|
55
|
+
error: vitest_1.vi.fn()
|
|
56
|
+
};
|
|
57
|
+
const error = new Error('Database connection failed');
|
|
58
|
+
const response = (0, responses_1.handleFunctionError)(error, mockContext);
|
|
59
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.INTERNAL_ERROR);
|
|
60
|
+
(0, vitest_1.expect)(response.jsonBody).toEqual({ error: 'Internal server error' });
|
|
61
|
+
(0, vitest_1.expect)(mockContext.error).toHaveBeenCalledWith('Function error: Database connection failed');
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)('handles non-Error objects', () => {
|
|
64
|
+
const mockContext = {
|
|
65
|
+
error: vitest_1.vi.fn()
|
|
66
|
+
};
|
|
67
|
+
const response = (0, responses_1.handleFunctionError)('string error', mockContext);
|
|
68
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.INTERNAL_ERROR);
|
|
69
|
+
(0, vitest_1.expect)(mockContext.error).toHaveBeenCalledWith('Function error: Unknown error');
|
|
70
|
+
});
|
|
71
|
+
(0, vitest_1.it)('handles null/undefined errors', () => {
|
|
72
|
+
const mockContext = {
|
|
73
|
+
error: vitest_1.vi.fn()
|
|
74
|
+
};
|
|
75
|
+
const response = (0, responses_1.handleFunctionError)(null, mockContext);
|
|
76
|
+
(0, vitest_1.expect)(response.status).toBe(status_1.HTTP_STATUS.INTERNAL_ERROR);
|
|
77
|
+
(0, vitest_1.expect)(mockContext.error).toHaveBeenCalledWith('Function error: Unknown error');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.describe)('isNotFoundError', () => {
|
|
81
|
+
(0, vitest_1.it)('returns true for 404 error', () => {
|
|
82
|
+
const error = Object.assign(new Error('Not found'), { statusCode: 404 });
|
|
83
|
+
(0, vitest_1.expect)((0, responses_1.isNotFoundError)(error)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
(0, vitest_1.it)('returns false for other status codes', () => {
|
|
86
|
+
const error = Object.assign(new Error('Conflict'), { statusCode: 409 });
|
|
87
|
+
(0, vitest_1.expect)((0, responses_1.isNotFoundError)(error)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.it)('returns false for non-Error objects', () => {
|
|
90
|
+
(0, vitest_1.expect)((0, responses_1.isNotFoundError)({ statusCode: 404 })).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)('returns false for errors without statusCode', () => {
|
|
93
|
+
(0, vitest_1.expect)((0, responses_1.isNotFoundError)(new Error('Not found'))).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.describe)('isConflictError', () => {
|
|
97
|
+
(0, vitest_1.it)('returns true for 409 error', () => {
|
|
98
|
+
const error = Object.assign(new Error('Conflict'), { statusCode: 409 });
|
|
99
|
+
(0, vitest_1.expect)((0, responses_1.isConflictError)(error)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.it)('returns false for other status codes', () => {
|
|
102
|
+
const error = Object.assign(new Error('Not found'), { statusCode: 404 });
|
|
103
|
+
(0, vitest_1.expect)((0, responses_1.isConflictError)(error)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)('returns false for non-Error objects', () => {
|
|
106
|
+
(0, vitest_1.expect)((0, responses_1.isConflictError)({ statusCode: 409 })).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)('returns false for errors without statusCode', () => {
|
|
109
|
+
(0, vitest_1.expect)((0, responses_1.isConflictError)(new Error('Conflict'))).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|