@markwharton/pwa-core 1.1.0 → 1.2.1
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/apiKey.test.d.ts +1 -0
- package/dist/__tests__/auth/apiKey.test.js +80 -0
- package/dist/__tests__/auth/token.test.d.ts +1 -0
- package/dist/__tests__/auth/token.test.js +189 -0
- package/dist/__tests__/auth/types.test.d.ts +1 -0
- package/dist/__tests__/auth/types.test.js +77 -0
- package/dist/__tests__/client/api.test.d.ts +1 -0
- package/dist/__tests__/client/api.test.js +269 -0
- package/dist/__tests__/client/apiError.test.d.ts +1 -0
- package/dist/__tests__/client/apiError.test.js +58 -0
- package/dist/__tests__/http/responses.test.d.ts +1 -0
- package/dist/__tests__/http/responses.test.js +112 -0
- package/dist/__tests__/http/status.test.d.ts +1 -0
- package/dist/__tests__/http/status.test.js +27 -0
- package/dist/__tests__/storage/client.test.d.ts +1 -0
- package/dist/__tests__/storage/client.test.js +173 -0
- package/dist/__tests__/storage/keys.test.d.ts +1 -0
- package/dist/__tests__/storage/keys.test.js +47 -0
- package/dist/__tests__/types.test.d.ts +1 -0
- package/dist/__tests__/types.test.js +56 -0
- package/dist/auth/apiKey.d.ts +24 -7
- package/dist/auth/apiKey.js +24 -7
- package/dist/auth/token.d.ts +37 -10
- package/dist/auth/token.js +37 -10
- package/dist/auth/types.d.ts +21 -3
- package/dist/auth/types.js +21 -3
- package/dist/client/api.d.ts +70 -9
- package/dist/client/api.js +70 -9
- package/dist/client/apiError.d.ts +22 -5
- package/dist/client/apiError.js +22 -5
- package/dist/http/responses.d.ts +57 -8
- package/dist/http/responses.js +57 -8
- package/dist/storage/client.d.ts +29 -6
- package/dist/storage/client.js +29 -6
- package/dist/types.d.ts +16 -3
- package/dist/types.js +16 -3
- package/package.json +22 -2
|
@@ -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("../../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,189 @@
|
|
|
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("../../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('../../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('../../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('../../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('../../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('../../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('../../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('../../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('../../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('../../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('../../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('../../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
|
+
});
|
|
138
|
+
(0, vitest_1.describe)('generateToken', () => {
|
|
139
|
+
(0, vitest_1.it)('generates valid JWT', async () => {
|
|
140
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
141
|
+
initAuth(validSecret);
|
|
142
|
+
const token = generateToken({ userId: '123' });
|
|
143
|
+
(0, vitest_1.expect)(token).toBeTruthy();
|
|
144
|
+
(0, vitest_1.expect)(token.split('.')).toHaveLength(3);
|
|
145
|
+
});
|
|
146
|
+
(0, vitest_1.it)('includes payload in token', async () => {
|
|
147
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
148
|
+
initAuth(validSecret);
|
|
149
|
+
const token = generateToken({ userId: '123', role: 'admin' });
|
|
150
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
151
|
+
(0, vitest_1.expect)(decoded.userId).toBe('123');
|
|
152
|
+
(0, vitest_1.expect)(decoded.role).toBe('admin');
|
|
153
|
+
});
|
|
154
|
+
(0, vitest_1.it)('uses default 7d expiration', async () => {
|
|
155
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
156
|
+
initAuth(validSecret);
|
|
157
|
+
const token = generateToken({ userId: '123' });
|
|
158
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
159
|
+
const expiresInSeconds = decoded.exp - decoded.iat;
|
|
160
|
+
(0, vitest_1.expect)(expiresInSeconds).toBe(7 * 24 * 60 * 60); // 7 days
|
|
161
|
+
});
|
|
162
|
+
(0, vitest_1.it)('accepts custom expiration', async () => {
|
|
163
|
+
const { initAuth, generateToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
164
|
+
initAuth(validSecret);
|
|
165
|
+
const token = generateToken({ userId: '123' }, '1h');
|
|
166
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
167
|
+
const expiresInSeconds = decoded.exp - decoded.iat;
|
|
168
|
+
(0, vitest_1.expect)(expiresInSeconds).toBe(60 * 60); // 1 hour
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
(0, vitest_1.describe)('generateLongLivedToken', () => {
|
|
172
|
+
(0, vitest_1.it)('generates token with 10-year default expiration', async () => {
|
|
173
|
+
const { initAuth, generateLongLivedToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
174
|
+
initAuth(validSecret);
|
|
175
|
+
const token = generateLongLivedToken({ userId: '123' });
|
|
176
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
177
|
+
const expiresInDays = (decoded.exp - decoded.iat) / (24 * 60 * 60);
|
|
178
|
+
(0, vitest_1.expect)(expiresInDays).toBe(3650);
|
|
179
|
+
});
|
|
180
|
+
(0, vitest_1.it)('accepts custom expiration in days', async () => {
|
|
181
|
+
const { initAuth, generateLongLivedToken } = await Promise.resolve().then(() => __importStar(require('../../auth/token')));
|
|
182
|
+
initAuth(validSecret);
|
|
183
|
+
const token = generateLongLivedToken({ userId: '123' }, 365);
|
|
184
|
+
const decoded = jsonwebtoken_1.default.verify(token, validSecret);
|
|
185
|
+
const expiresInDays = (decoded.exp - decoded.iat) / (24 * 60 * 60);
|
|
186
|
+
(0, vitest_1.expect)(expiresInDays).toBe(365);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const types_1 = require("../../auth/types");
|
|
5
|
+
(0, vitest_1.describe)('Auth type guards', () => {
|
|
6
|
+
const basePayload = {
|
|
7
|
+
iat: 1234567890,
|
|
8
|
+
exp: 1234567890 + 3600
|
|
9
|
+
};
|
|
10
|
+
(0, vitest_1.describe)('hasUsername', () => {
|
|
11
|
+
(0, vitest_1.it)('returns true for payload with username', () => {
|
|
12
|
+
const payload = { ...basePayload, username: 'testuser' };
|
|
13
|
+
(0, vitest_1.expect)((0, types_1.hasUsername)(payload)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)('returns false for payload without username', () => {
|
|
16
|
+
(0, vitest_1.expect)((0, types_1.hasUsername)(basePayload)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)('returns false for payload with non-string username', () => {
|
|
19
|
+
const payload = { ...basePayload, username: 123 };
|
|
20
|
+
(0, vitest_1.expect)((0, types_1.hasUsername)(payload)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)('returns false for payload with empty string username', () => {
|
|
23
|
+
const payload = { ...basePayload, username: '' };
|
|
24
|
+
(0, vitest_1.expect)((0, types_1.hasUsername)(payload)).toBe(true); // Empty string is still a string
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
(0, vitest_1.describe)('hasRole', () => {
|
|
28
|
+
(0, vitest_1.it)('returns true for payload with role', () => {
|
|
29
|
+
const payload = {
|
|
30
|
+
...basePayload,
|
|
31
|
+
authenticated: true,
|
|
32
|
+
tokenType: 'user',
|
|
33
|
+
role: 'admin'
|
|
34
|
+
};
|
|
35
|
+
(0, vitest_1.expect)((0, types_1.hasRole)(payload)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
(0, vitest_1.it)('returns false for payload without role', () => {
|
|
38
|
+
(0, vitest_1.expect)((0, types_1.hasRole)(basePayload)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)('returns true for viewer role', () => {
|
|
41
|
+
const payload = {
|
|
42
|
+
...basePayload,
|
|
43
|
+
authenticated: true,
|
|
44
|
+
tokenType: 'user',
|
|
45
|
+
role: 'viewer'
|
|
46
|
+
};
|
|
47
|
+
(0, vitest_1.expect)((0, types_1.hasRole)(payload)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.describe)('isAdmin', () => {
|
|
51
|
+
(0, vitest_1.it)('returns true for admin role', () => {
|
|
52
|
+
const payload = {
|
|
53
|
+
...basePayload,
|
|
54
|
+
authenticated: true,
|
|
55
|
+
tokenType: 'user',
|
|
56
|
+
role: 'admin'
|
|
57
|
+
};
|
|
58
|
+
(0, vitest_1.expect)((0, types_1.isAdmin)(payload)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('returns false for viewer role', () => {
|
|
61
|
+
const payload = {
|
|
62
|
+
...basePayload,
|
|
63
|
+
authenticated: true,
|
|
64
|
+
tokenType: 'user',
|
|
65
|
+
role: 'viewer'
|
|
66
|
+
};
|
|
67
|
+
(0, vitest_1.expect)((0, types_1.isAdmin)(payload)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('returns false for payload without role', () => {
|
|
70
|
+
(0, vitest_1.expect)((0, types_1.isAdmin)(basePayload)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)('returns false for payload with username but no role', () => {
|
|
73
|
+
const payload = { ...basePayload, username: 'testuser' };
|
|
74
|
+
(0, vitest_1.expect)((0, types_1.isAdmin)(payload)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const vitest_1 = require("vitest");
|
|
37
|
+
// Mock fetch globally
|
|
38
|
+
const mockFetch = vitest_1.vi.fn();
|
|
39
|
+
global.fetch = mockFetch;
|
|
40
|
+
(0, vitest_1.describe)('API client', () => {
|
|
41
|
+
(0, vitest_1.beforeEach)(() => {
|
|
42
|
+
vitest_1.vi.resetModules();
|
|
43
|
+
mockFetch.mockReset();
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.describe)('initApiClient', () => {
|
|
46
|
+
(0, vitest_1.it)('sets token getter', async () => {
|
|
47
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
48
|
+
initApiClient({ getToken: () => 'test-token' });
|
|
49
|
+
mockFetch.mockResolvedValue({
|
|
50
|
+
ok: true,
|
|
51
|
+
text: () => Promise.resolve('{"data": "test"}')
|
|
52
|
+
});
|
|
53
|
+
await apiCall('/api/test');
|
|
54
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/test', vitest_1.expect.objectContaining({
|
|
55
|
+
headers: vitest_1.expect.objectContaining({
|
|
56
|
+
Authorization: 'Bearer test-token'
|
|
57
|
+
})
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('sets onUnauthorized callback', async () => {
|
|
61
|
+
const onUnauthorized = vitest_1.vi.fn();
|
|
62
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
63
|
+
initApiClient({ getToken: () => 'token', onUnauthorized });
|
|
64
|
+
mockFetch.mockResolvedValue({
|
|
65
|
+
ok: false,
|
|
66
|
+
status: 401,
|
|
67
|
+
json: () => Promise.resolve({ error: 'Unauthorized' })
|
|
68
|
+
});
|
|
69
|
+
await (0, vitest_1.expect)(apiCall('/api/test')).rejects.toThrow();
|
|
70
|
+
(0, vitest_1.expect)(onUnauthorized).toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.describe)('apiCall', () => {
|
|
74
|
+
(0, vitest_1.it)('makes authenticated request', async () => {
|
|
75
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
76
|
+
initApiClient({ getToken: () => 'my-token' });
|
|
77
|
+
mockFetch.mockResolvedValue({
|
|
78
|
+
ok: true,
|
|
79
|
+
text: () => Promise.resolve('{"result": "success"}')
|
|
80
|
+
});
|
|
81
|
+
const result = await apiCall('/api/endpoint');
|
|
82
|
+
(0, vitest_1.expect)(result).toEqual({ result: 'success' });
|
|
83
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/endpoint', vitest_1.expect.objectContaining({
|
|
84
|
+
headers: vitest_1.expect.objectContaining({
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
Authorization: 'Bearer my-token'
|
|
87
|
+
})
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)('handles request without token', async () => {
|
|
91
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
92
|
+
initApiClient({ getToken: () => null });
|
|
93
|
+
mockFetch.mockResolvedValue({
|
|
94
|
+
ok: true,
|
|
95
|
+
text: () => Promise.resolve('{}')
|
|
96
|
+
});
|
|
97
|
+
await apiCall('/api/public');
|
|
98
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
99
|
+
(0, vitest_1.expect)(headers.Authorization).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.it)('throws ApiError on non-ok response', async () => {
|
|
102
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
103
|
+
const { ApiError: LocalApiError } = await Promise.resolve().then(() => __importStar(require('../../client/apiError')));
|
|
104
|
+
initApiClient({ getToken: () => 'token' });
|
|
105
|
+
mockFetch.mockResolvedValue({
|
|
106
|
+
ok: false,
|
|
107
|
+
status: 400,
|
|
108
|
+
json: () => Promise.resolve({ error: 'Invalid input' })
|
|
109
|
+
});
|
|
110
|
+
await (0, vitest_1.expect)(apiCall('/api/test')).rejects.toThrow(LocalApiError);
|
|
111
|
+
await (0, vitest_1.expect)(apiCall('/api/test')).rejects.toMatchObject({
|
|
112
|
+
status: 400,
|
|
113
|
+
message: 'Invalid input'
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
(0, vitest_1.it)('handles empty response body', async () => {
|
|
117
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
118
|
+
initApiClient({ getToken: () => 'token' });
|
|
119
|
+
mockFetch.mockResolvedValue({
|
|
120
|
+
ok: true,
|
|
121
|
+
text: () => Promise.resolve('')
|
|
122
|
+
});
|
|
123
|
+
const result = await apiCall('/api/delete');
|
|
124
|
+
(0, vitest_1.expect)(result).toEqual({});
|
|
125
|
+
});
|
|
126
|
+
(0, vitest_1.it)('handles JSON parse error in error response', async () => {
|
|
127
|
+
const { initApiClient, apiCall } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
128
|
+
initApiClient({ getToken: () => 'token' });
|
|
129
|
+
mockFetch.mockResolvedValue({
|
|
130
|
+
ok: false,
|
|
131
|
+
status: 500,
|
|
132
|
+
json: () => Promise.reject(new Error('Invalid JSON'))
|
|
133
|
+
});
|
|
134
|
+
await (0, vitest_1.expect)(apiCall('/api/test')).rejects.toMatchObject({
|
|
135
|
+
status: 500,
|
|
136
|
+
message: 'Request failed'
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.describe)('HTTP method helpers', () => {
|
|
141
|
+
(0, vitest_1.beforeEach)(async () => {
|
|
142
|
+
const { initApiClient } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
143
|
+
initApiClient({ getToken: () => 'token' });
|
|
144
|
+
mockFetch.mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
text: () => Promise.resolve('{"data": "test"}')
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
(0, vitest_1.it)('apiGet uses GET method', async () => {
|
|
150
|
+
const { apiGet } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
151
|
+
await apiGet('/api/resource');
|
|
152
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
153
|
+
method: 'GET'
|
|
154
|
+
}));
|
|
155
|
+
});
|
|
156
|
+
(0, vitest_1.it)('apiPost uses POST method with body', async () => {
|
|
157
|
+
const { apiPost } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
158
|
+
await apiPost('/api/resource', { name: 'test' });
|
|
159
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: '{"name":"test"}'
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
(0, vitest_1.it)('apiPost works without body', async () => {
|
|
165
|
+
const { apiPost } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
166
|
+
await apiPost('/api/resource');
|
|
167
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: undefined
|
|
170
|
+
}));
|
|
171
|
+
});
|
|
172
|
+
(0, vitest_1.it)('apiPut uses PUT method with body', async () => {
|
|
173
|
+
const { apiPut } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
174
|
+
await apiPut('/api/resource', { name: 'updated' });
|
|
175
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
176
|
+
method: 'PUT',
|
|
177
|
+
body: '{"name":"updated"}'
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
(0, vitest_1.it)('apiPatch uses PATCH method with body', async () => {
|
|
181
|
+
const { apiPatch } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
182
|
+
await apiPatch('/api/resource', { field: 'value' });
|
|
183
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
184
|
+
method: 'PATCH',
|
|
185
|
+
body: '{"field":"value"}'
|
|
186
|
+
}));
|
|
187
|
+
});
|
|
188
|
+
(0, vitest_1.it)('apiDelete uses DELETE method', async () => {
|
|
189
|
+
const { apiDelete } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
190
|
+
await apiDelete('/api/resource');
|
|
191
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('/api/resource', vitest_1.expect.objectContaining({
|
|
192
|
+
method: 'DELETE'
|
|
193
|
+
}));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
(0, vitest_1.describe)('apiCallSafe', () => {
|
|
197
|
+
(0, vitest_1.it)('returns ok response with data', async () => {
|
|
198
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
199
|
+
initApiClient({ getToken: () => 'token' });
|
|
200
|
+
mockFetch.mockResolvedValue({
|
|
201
|
+
ok: true,
|
|
202
|
+
status: 200,
|
|
203
|
+
text: () => Promise.resolve('{"user": "john"}')
|
|
204
|
+
});
|
|
205
|
+
const result = await apiCallSafe('/api/user');
|
|
206
|
+
(0, vitest_1.expect)(result.ok).toBe(true);
|
|
207
|
+
(0, vitest_1.expect)(result.status).toBe(200);
|
|
208
|
+
(0, vitest_1.expect)(result.data).toEqual({ user: 'john' });
|
|
209
|
+
});
|
|
210
|
+
(0, vitest_1.it)('preserves actual status code', async () => {
|
|
211
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
212
|
+
initApiClient({ getToken: () => 'token' });
|
|
213
|
+
mockFetch.mockResolvedValue({
|
|
214
|
+
ok: true,
|
|
215
|
+
status: 201,
|
|
216
|
+
text: () => Promise.resolve('{"id": "123"}')
|
|
217
|
+
});
|
|
218
|
+
const result = await apiCallSafe('/api/create');
|
|
219
|
+
(0, vitest_1.expect)(result.status).toBe(201);
|
|
220
|
+
});
|
|
221
|
+
(0, vitest_1.it)('returns error response for non-ok status', async () => {
|
|
222
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
223
|
+
initApiClient({ getToken: () => 'token' });
|
|
224
|
+
mockFetch.mockResolvedValue({
|
|
225
|
+
ok: false,
|
|
226
|
+
status: 404,
|
|
227
|
+
json: () => Promise.resolve({ error: 'Not found' })
|
|
228
|
+
});
|
|
229
|
+
const result = await apiCallSafe('/api/missing');
|
|
230
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
231
|
+
(0, vitest_1.expect)(result.status).toBe(404);
|
|
232
|
+
(0, vitest_1.expect)(result.error).toBe('Not found');
|
|
233
|
+
});
|
|
234
|
+
(0, vitest_1.it)('handles network errors', async () => {
|
|
235
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
236
|
+
initApiClient({ getToken: () => 'token' });
|
|
237
|
+
mockFetch.mockRejectedValue(new Error('Network failure'));
|
|
238
|
+
const result = await apiCallSafe('/api/test');
|
|
239
|
+
(0, vitest_1.expect)(result.ok).toBe(false);
|
|
240
|
+
(0, vitest_1.expect)(result.status).toBe(0);
|
|
241
|
+
(0, vitest_1.expect)(result.error).toBe('Network error');
|
|
242
|
+
});
|
|
243
|
+
(0, vitest_1.it)('calls onUnauthorized for 401', async () => {
|
|
244
|
+
const onUnauthorized = vitest_1.vi.fn();
|
|
245
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
246
|
+
initApiClient({ getToken: () => 'token', onUnauthorized });
|
|
247
|
+
mockFetch.mockResolvedValue({
|
|
248
|
+
ok: false,
|
|
249
|
+
status: 401,
|
|
250
|
+
json: () => Promise.resolve({ error: 'Token expired' })
|
|
251
|
+
});
|
|
252
|
+
await apiCallSafe('/api/protected');
|
|
253
|
+
(0, vitest_1.expect)(onUnauthorized).toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
(0, vitest_1.it)('handles empty success response', async () => {
|
|
256
|
+
const { initApiClient, apiCallSafe } = await Promise.resolve().then(() => __importStar(require('../../client/api')));
|
|
257
|
+
initApiClient({ getToken: () => 'token' });
|
|
258
|
+
mockFetch.mockResolvedValue({
|
|
259
|
+
ok: true,
|
|
260
|
+
status: 204,
|
|
261
|
+
text: () => Promise.resolve('')
|
|
262
|
+
});
|
|
263
|
+
const result = await apiCallSafe('/api/delete');
|
|
264
|
+
(0, vitest_1.expect)(result.ok).toBe(true);
|
|
265
|
+
(0, vitest_1.expect)(result.status).toBe(204);
|
|
266
|
+
(0, vitest_1.expect)(result.data).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|