@unifiedmemory/cli 1.3.12 → 1.3.14

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.
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Unit tests for lib/token-validation.js
3
3
  *
4
- * Tests token loading, expiration checking, and org claims recovery.
4
+ * Tests token loading and expiration checking.
5
+ * The implementation is simple: load token, refresh if expired.
5
6
  */
6
7
 
7
8
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8
9
 
9
- // Mock dependencies - using factory functions to avoid hoisting issues
10
+ // Mock dependencies
10
11
  vi.mock('../../lib/token-storage.js', () => ({
11
12
  getToken: vi.fn(),
12
- saveToken: vi.fn(),
13
13
  }));
14
14
 
15
15
  vi.mock('../../lib/token-refresh.js', () => ({
@@ -17,28 +17,10 @@ vi.mock('../../lib/token-refresh.js', () => ({
17
17
  refreshAccessToken: vi.fn(),
18
18
  }));
19
19
 
20
- vi.mock('../../lib/clerk-api.js', () => ({
21
- getOrgScopedToken: vi.fn(),
22
- }));
23
-
24
- vi.mock('../../lib/jwt-utils.js', () => ({
25
- parseJWT: vi.fn(),
26
- }));
27
-
28
- vi.mock('../../lib/config.js', () => ({
29
- config: {
30
- clerkDomain: 'clerk.test.com',
31
- clerkClientId: 'test_client_id',
32
- clerkClientSecret: 'test_client_secret',
33
- },
34
- }));
35
-
36
20
  // Import after mocking
37
21
  import { loadAndRefreshToken } from '../../lib/token-validation.js';
38
- import { getToken, saveToken } from '../../lib/token-storage.js';
22
+ import { getToken } from '../../lib/token-storage.js';
39
23
  import { isTokenExpired, refreshAccessToken } from '../../lib/token-refresh.js';
40
- import { getOrgScopedToken } from '../../lib/clerk-api.js';
41
- import { parseJWT } from '../../lib/jwt-utils.js';
42
24
 
43
25
  describe('loadAndRefreshToken', () => {
44
26
  const futureExp = Math.floor(Date.now() / 1000) + 3600;
@@ -130,238 +112,39 @@ describe('loadAndRefreshToken', () => {
130
112
  });
131
113
  });
132
114
 
133
- describe('org claims recovery', () => {
134
- it('recovers using decoded.sid when org claims missing', async () => {
135
- const tokenMissingOrgClaims = {
136
- idToken: 'token_no_org',
137
- decoded: { exp: futureExp, sid: 'sess_123' },
138
- selectedOrg: { id: 'org_456', name: 'Test Org' },
139
- };
140
-
141
- vi.mocked(getToken).mockReturnValue(tokenMissingOrgClaims);
142
- vi.mocked(getOrgScopedToken).mockResolvedValue({
143
- jwt: 'org_scoped_jwt',
144
- });
145
- vi.mocked(parseJWT).mockReturnValue({
146
- exp: futureExp,
147
- o: { o_id: 'org_456' },
148
- });
149
-
150
- await loadAndRefreshToken();
151
-
152
- expect(getOrgScopedToken).toHaveBeenCalledWith(
153
- 'sess_123',
154
- 'org_456',
155
- 'token_no_org'
156
- );
157
- expect(saveToken).toHaveBeenCalled();
158
- });
159
-
160
- it('recovers using originalSid when decoded.sid is missing', async () => {
161
- const tokenWithOriginalSid = {
162
- idToken: 'token_org_scoped',
163
- decoded: { exp: futureExp }, // No sid - org-scoped token
164
- selectedOrg: { id: 'org_456', name: 'Test Org' },
165
- originalSid: 'sess_original',
166
- };
167
-
168
- vi.mocked(getToken).mockReturnValue(tokenWithOriginalSid);
169
- vi.mocked(getOrgScopedToken).mockResolvedValue({
170
- jwt: 'new_org_scoped_jwt',
171
- });
172
- vi.mocked(parseJWT).mockReturnValue({
173
- exp: futureExp,
174
- o: { o_id: 'org_456' },
175
- });
176
-
177
- await loadAndRefreshToken();
178
-
179
- expect(getOrgScopedToken).toHaveBeenCalledWith(
180
- 'sess_original',
181
- 'org_456',
182
- 'token_org_scoped'
183
- );
184
- });
185
-
186
- it('skips recovery when token already has org claims', async () => {
187
- const tokenWithOrgClaims = {
188
- idToken: 'complete_token',
189
- decoded: { exp: futureExp, o: { o_id: 'org_456' } },
190
- selectedOrg: { id: 'org_456', name: 'Test Org' },
191
- };
192
-
193
- vi.mocked(getToken).mockReturnValue(tokenWithOrgClaims);
194
-
195
- const result = await loadAndRefreshToken();
196
-
197
- expect(getOrgScopedToken).not.toHaveBeenCalled();
198
- expect(result.decoded.o).toBeDefined();
199
- });
200
-
201
- it('skips recovery when no selectedOrg (personal account)', async () => {
202
- const personalToken = {
203
- idToken: 'personal_token',
204
- decoded: { exp: futureExp, sid: 'sess_123' },
205
- // No selectedOrg - personal account
206
- };
207
-
208
- vi.mocked(getToken).mockReturnValue(personalToken);
209
-
210
- await loadAndRefreshToken();
211
-
212
- expect(getOrgScopedToken).not.toHaveBeenCalled();
213
- });
214
-
215
- it('skips recovery when selectedOrg has no id', async () => {
216
- const tokenWithEmptyOrg = {
217
- idToken: 'token_empty_org',
218
- decoded: { exp: futureExp, sid: 'sess_123' },
219
- selectedOrg: {}, // Empty org object
220
- };
221
-
222
- vi.mocked(getToken).mockReturnValue(tokenWithEmptyOrg);
223
-
224
- await loadAndRefreshToken();
225
-
226
- expect(getOrgScopedToken).not.toHaveBeenCalled();
227
- });
228
-
229
- it('continues gracefully when recovery fails', async () => {
230
- const brokenToken = {
231
- idToken: 'broken_token',
232
- decoded: { exp: futureExp, sid: 'sess_123' },
233
- selectedOrg: { id: 'org_456', name: 'Test Org' },
234
- };
235
-
236
- vi.mocked(getToken).mockReturnValue(brokenToken);
237
- vi.mocked(getOrgScopedToken).mockRejectedValue(new Error('API error'));
238
-
239
- // Should not throw, just log warning and return token
240
- const result = await loadAndRefreshToken();
241
-
242
- expect(result).toBeDefined();
243
- expect(result.idToken).toBe('broken_token');
244
- });
245
-
246
- it('saves updated token after successful recovery', async () => {
247
- const tokenMissingOrgClaims = {
248
- idToken: 'token_no_org',
249
- decoded: { exp: futureExp, sid: 'sess_123' },
250
- selectedOrg: { id: 'org_456', name: 'Test Org' },
251
- refresh_token: 'rt_123',
252
- };
253
-
254
- vi.mocked(getToken).mockReturnValue(tokenMissingOrgClaims);
255
- vi.mocked(getOrgScopedToken).mockResolvedValue({
256
- jwt: 'org_scoped_jwt',
257
- });
258
- vi.mocked(parseJWT).mockReturnValue({
259
- exp: futureExp,
260
- o: { o_id: 'org_456' },
261
- });
262
-
263
- await loadAndRefreshToken();
264
-
265
- expect(saveToken).toHaveBeenCalledWith(
266
- expect.objectContaining({
267
- idToken: 'org_scoped_jwt',
268
- })
269
- );
270
- });
271
- });
272
-
273
- describe('OAuth refresh fallback for missing sid', () => {
274
- const originalFetch = global.fetch;
275
-
276
- afterEach(() => {
277
- global.fetch = originalFetch;
278
- });
279
-
280
- it('attempts OAuth refresh when no sid/originalSid but has refresh_token', async () => {
281
- const tokenNoSid = {
282
- idToken: 'token_no_sid',
283
- decoded: { exp: futureExp }, // No sid
284
- selectedOrg: { id: 'org_456', name: 'Test Org' },
115
+ describe('valid token handling', () => {
116
+ it('returns token data when not expired', async () => {
117
+ const validToken = {
118
+ idToken: 'valid_token',
119
+ decoded: { exp: futureExp, sub: 'user_123' },
285
120
  refresh_token: 'rt_123',
286
- // No originalSid either
287
121
  };
288
122
 
289
- vi.mocked(getToken).mockReturnValue(tokenNoSid);
290
-
291
- // Mock fetch for OAuth refresh
292
- global.fetch = vi.fn().mockResolvedValue({
293
- ok: true,
294
- json: () => Promise.resolve({
295
- access_token: 'new_at',
296
- id_token: 'new_idt_with_sid',
297
- refresh_token: 'new_rt',
298
- }),
299
- });
300
-
301
- // parseJWT returns token with sid after refresh
302
- vi.mocked(parseJWT)
303
- .mockReturnValueOnce({ exp: futureExp, sid: 'sess_new' }) // First call: parse refreshed token
304
- .mockReturnValueOnce({ exp: futureExp, o: { o_id: 'org_456' } }); // Second call: parse org-scoped token
305
-
306
- vi.mocked(getOrgScopedToken).mockResolvedValue({
307
- jwt: 'org_scoped_jwt_after_refresh',
308
- });
309
-
310
- await loadAndRefreshToken();
311
-
312
- expect(global.fetch).toHaveBeenCalledWith(
313
- expect.stringContaining('/oauth/token'),
314
- expect.any(Object)
315
- );
316
- });
317
-
318
- it('continues without org token when OAuth refresh fails', async () => {
319
- const tokenNoSid = {
320
- idToken: 'token_no_sid',
321
- decoded: { exp: futureExp },
322
- selectedOrg: { id: 'org_456', name: 'Test Org' },
323
- refresh_token: 'rt_123',
324
- };
123
+ vi.mocked(getToken).mockReturnValue(validToken);
124
+ vi.mocked(isTokenExpired).mockReturnValue(false);
325
125
 
326
- vi.mocked(getToken).mockReturnValue(tokenNoSid);
327
-
328
- // Mock fetch to fail
329
- global.fetch = vi.fn().mockResolvedValue({
330
- ok: false,
331
- status: 400,
332
- });
333
-
334
- // Should not throw, just return token as-is
335
126
  const result = await loadAndRefreshToken();
336
127
 
337
- expect(result).toBeDefined();
338
- expect(result.idToken).toBe('token_no_sid');
128
+ expect(refreshAccessToken).not.toHaveBeenCalled();
129
+ expect(result).toEqual(validToken);
339
130
  });
340
131
 
341
- it('logs warning when no recovery path available', async () => {
342
- const fullyBrokenToken = {
343
- idToken: 'broken_token',
344
- decoded: { exp: futureExp },
345
- selectedOrg: { id: 'org_456', name: 'Test Org' },
346
- // No sid, no originalSid, no refresh_token
132
+ it('returns token with org context when present', async () => {
133
+ const orgToken = {
134
+ idToken: 'org_token',
135
+ decoded: { exp: futureExp, sub: 'user_123', org_id: 'org_456' },
136
+ selectedOrgId: 'org_456',
137
+ selectedOrgName: 'Test Org',
138
+ refresh_token: 'rt_123',
347
139
  };
348
140
 
349
- vi.mocked(getToken).mockReturnValue(fullyBrokenToken);
350
-
351
- // Spy on console.error
352
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
141
+ vi.mocked(getToken).mockReturnValue(orgToken);
142
+ vi.mocked(isTokenExpired).mockReturnValue(false);
353
143
 
354
144
  const result = await loadAndRefreshToken();
355
145
 
356
- // Should still return token (API calls may fail later)
357
- expect(result).toBeDefined();
358
-
359
- // Should have logged warning about missing session ID
360
- expect(consoleSpy).toHaveBeenCalledWith(
361
- expect.stringContaining('Could not obtain session ID')
362
- );
363
-
364
- consoleSpy.mockRestore();
146
+ expect(result.selectedOrgId).toBe('org_456');
147
+ expect(result.selectedOrgName).toBe('Test Org');
365
148
  });
366
149
  });
367
150
  });