@unifiedmemory/cli 1.3.0 → 1.3.6
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/.github/workflows/test-and-publish.yml +92 -0
- package/commands/record.js +35 -13
- package/lib/mcp-proxy.js +26 -0
- package/lib/memory-instructions.js +59 -49
- package/package.json +16 -4
- package/tests/fixtures/expired-auth.json +20 -0
- package/tests/fixtures/mock-auth.json +21 -0
- package/tests/fixtures/mock-project-config.json +12 -0
- package/tests/mocks/api.mock.js +200 -0
- package/tests/mocks/token-storage.mock.js +79 -0
- package/tests/setup.js +29 -0
- package/tests/unit/config.test.js +165 -0
- package/tests/unit/jwt-utils.test.js +217 -0
- package/tests/unit/mcp-proxy.test.js +459 -0
- package/tests/unit/provider-detector.test.js +344 -0
- package/tests/unit/token-storage.test.js +138 -0
- package/vitest.config.js +37 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for token-storage.js
|
|
3
|
+
*
|
|
4
|
+
* Provides mock implementations of token storage functions
|
|
5
|
+
* that don't interact with the real filesystem.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { vi } from 'vitest';
|
|
9
|
+
import mockAuthData from '../fixtures/mock-auth.json' with { type: 'json' };
|
|
10
|
+
import expiredAuthData from '../fixtures/expired-auth.json' with { type: 'json' };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a mock token storage module with configurable behavior
|
|
14
|
+
* @param {Object} options - Configuration options
|
|
15
|
+
* @param {Object|null} options.initialToken - Initial token data to return (use null for no token)
|
|
16
|
+
* @param {boolean} options.expired - Whether to use expired token fixture
|
|
17
|
+
* @param {boolean} options.empty - Whether to start with no token
|
|
18
|
+
* @returns {Object} Mock token storage functions
|
|
19
|
+
*/
|
|
20
|
+
export function createTokenStorageMock(options = {}) {
|
|
21
|
+
let tokenData;
|
|
22
|
+
|
|
23
|
+
if (options.empty) {
|
|
24
|
+
tokenData = null;
|
|
25
|
+
} else if (options.expired) {
|
|
26
|
+
tokenData = { ...expiredAuthData };
|
|
27
|
+
} else if (options.initialToken !== undefined) {
|
|
28
|
+
tokenData = options.initialToken;
|
|
29
|
+
} else {
|
|
30
|
+
tokenData = { ...mockAuthData };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
saveToken: vi.fn((data) => {
|
|
35
|
+
tokenData = data;
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
getToken: vi.fn(() => tokenData),
|
|
39
|
+
|
|
40
|
+
clearToken: vi.fn(() => {
|
|
41
|
+
tokenData = null;
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
updateSelectedOrg: vi.fn((orgData) => {
|
|
45
|
+
if (tokenData) {
|
|
46
|
+
tokenData.selectedOrg = orgData;
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error('No token found. Please login first.');
|
|
49
|
+
}
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
getSelectedOrg: vi.fn(() => tokenData?.selectedOrg || null),
|
|
53
|
+
|
|
54
|
+
// Helper to reset state between tests
|
|
55
|
+
_reset: (newData = null) => {
|
|
56
|
+
tokenData = newData ?? { ...mockAuthData };
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Helper to get current state for assertions
|
|
60
|
+
_getState: () => tokenData,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates mock token storage that returns null (no token)
|
|
66
|
+
*/
|
|
67
|
+
export function createEmptyTokenStorageMock() {
|
|
68
|
+
return createTokenStorageMock({ empty: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates mock token storage with expired token
|
|
73
|
+
*/
|
|
74
|
+
export function createExpiredTokenStorageMock() {
|
|
75
|
+
return createTokenStorageMock({ expired: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Export fixtures for direct use in tests
|
|
79
|
+
export { mockAuthData, expiredAuthData };
|
package/tests/setup.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest Global Test Setup
|
|
3
|
+
*
|
|
4
|
+
* This file runs before all tests and sets up the test environment.
|
|
5
|
+
* It configures mocks and ensures tests don't interact with real auth.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { vi, beforeAll, afterAll, afterEach } from 'vitest';
|
|
9
|
+
|
|
10
|
+
// Ensure we're in test mode
|
|
11
|
+
process.env.NODE_ENV = 'test';
|
|
12
|
+
|
|
13
|
+
// Mock console.error to reduce noise in tests (optional)
|
|
14
|
+
// Uncomment if you want to suppress console output during tests:
|
|
15
|
+
// vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
16
|
+
// vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
// Global setup before all tests
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Clean up mocks after each test
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(() => {
|
|
28
|
+
// Global cleanup after all tests
|
|
29
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for lib/config.js
|
|
3
|
+
*
|
|
4
|
+
* Tests configuration loading and validation.
|
|
5
|
+
* Note: The config module loads environment variables at import time,
|
|
6
|
+
* so we test the validateConfig function and config structure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
|
|
11
|
+
describe('config', () => {
|
|
12
|
+
// Store original env vars
|
|
13
|
+
const originalEnv = { ...process.env };
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset modules to get fresh config import
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
// Restore original environment
|
|
22
|
+
process.env = { ...originalEnv };
|
|
23
|
+
vi.resetModules();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('default configuration', () => {
|
|
27
|
+
it('should have default values when no environment variables are set', async () => {
|
|
28
|
+
// Clear relevant env vars
|
|
29
|
+
delete process.env.CLERK_CLIENT_ID;
|
|
30
|
+
delete process.env.CLERK_DOMAIN;
|
|
31
|
+
delete process.env.API_ENDPOINT;
|
|
32
|
+
delete process.env.REDIRECT_URI;
|
|
33
|
+
delete process.env.PORT;
|
|
34
|
+
|
|
35
|
+
// Fresh import to get defaults
|
|
36
|
+
const { config } = await import('../../lib/config.js');
|
|
37
|
+
|
|
38
|
+
expect(config.clerkClientId).toBe('nULlnomaKB9rRGP2');
|
|
39
|
+
expect(config.clerkDomain).toBe('clear-caiman-45.clerk.accounts.dev');
|
|
40
|
+
expect(config.apiEndpoint).toBe('https://rose-asp-main-1c0b114.d2.zuplo.dev');
|
|
41
|
+
expect(config.redirectUri).toBe('http://localhost:3333/callback');
|
|
42
|
+
expect(config.port).toBe(3333);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should have clerkClientSecret as undefined by default', async () => {
|
|
46
|
+
delete process.env.CLERK_CLIENT_SECRET;
|
|
47
|
+
|
|
48
|
+
const { config } = await import('../../lib/config.js');
|
|
49
|
+
|
|
50
|
+
expect(config.clerkClientSecret).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('environment variable overrides', () => {
|
|
55
|
+
it('should use CLERK_CLIENT_ID from environment', async () => {
|
|
56
|
+
process.env.CLERK_CLIENT_ID = 'custom_client_id';
|
|
57
|
+
|
|
58
|
+
const { config } = await import('../../lib/config.js');
|
|
59
|
+
|
|
60
|
+
expect(config.clerkClientId).toBe('custom_client_id');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should use CLERK_DOMAIN from environment', async () => {
|
|
64
|
+
process.env.CLERK_DOMAIN = 'custom.clerk.dev';
|
|
65
|
+
|
|
66
|
+
const { config } = await import('../../lib/config.js');
|
|
67
|
+
|
|
68
|
+
expect(config.clerkDomain).toBe('custom.clerk.dev');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should use API_ENDPOINT from environment', async () => {
|
|
72
|
+
process.env.API_ENDPOINT = 'https://custom-api.example.com';
|
|
73
|
+
|
|
74
|
+
const { config } = await import('../../lib/config.js');
|
|
75
|
+
|
|
76
|
+
expect(config.apiEndpoint).toBe('https://custom-api.example.com');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should use REDIRECT_URI from environment', async () => {
|
|
80
|
+
process.env.REDIRECT_URI = 'http://localhost:8080/callback';
|
|
81
|
+
|
|
82
|
+
const { config } = await import('../../lib/config.js');
|
|
83
|
+
|
|
84
|
+
expect(config.redirectUri).toBe('http://localhost:8080/callback');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should use PORT from environment and parse as integer', async () => {
|
|
88
|
+
process.env.PORT = '8080';
|
|
89
|
+
|
|
90
|
+
const { config } = await import('../../lib/config.js');
|
|
91
|
+
|
|
92
|
+
expect(config.port).toBe(8080);
|
|
93
|
+
expect(typeof config.port).toBe('number');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should use CLERK_CLIENT_SECRET from environment', async () => {
|
|
97
|
+
process.env.CLERK_CLIENT_SECRET = 'secret_key_123';
|
|
98
|
+
|
|
99
|
+
const { config } = await import('../../lib/config.js');
|
|
100
|
+
|
|
101
|
+
expect(config.clerkClientSecret).toBe('secret_key_123');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('validateConfig', () => {
|
|
106
|
+
it('should return true for valid default configuration', async () => {
|
|
107
|
+
const { validateConfig } = await import('../../lib/config.js');
|
|
108
|
+
|
|
109
|
+
expect(validateConfig()).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should throw error for invalid API_ENDPOINT URL', async () => {
|
|
113
|
+
process.env.API_ENDPOINT = 'not-a-valid-url';
|
|
114
|
+
|
|
115
|
+
const { validateConfig } = await import('../../lib/config.js');
|
|
116
|
+
|
|
117
|
+
expect(() => validateConfig()).toThrow('API_ENDPOINT must be a valid URL');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should use default when CLERK_DOMAIN is empty (falsy falls back to default)', async () => {
|
|
121
|
+
process.env.CLERK_DOMAIN = '';
|
|
122
|
+
|
|
123
|
+
const { config, validateConfig } = await import('../../lib/config.js');
|
|
124
|
+
|
|
125
|
+
// Empty string is falsy, so it falls back to default
|
|
126
|
+
expect(config.clerkDomain).toBe('clear-caiman-45.clerk.accounts.dev');
|
|
127
|
+
expect(validateConfig()).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should throw error when CLERK_DOMAIN is whitespace only', async () => {
|
|
131
|
+
// Whitespace is truthy, so it will be used instead of default
|
|
132
|
+
process.env.CLERK_DOMAIN = ' ';
|
|
133
|
+
|
|
134
|
+
const { validateConfig } = await import('../../lib/config.js');
|
|
135
|
+
|
|
136
|
+
expect(() => validateConfig()).toThrow('CLERK_DOMAIN cannot be empty');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should use default when CLERK_CLIENT_ID is empty (falsy falls back to default)', async () => {
|
|
140
|
+
process.env.CLERK_CLIENT_ID = '';
|
|
141
|
+
|
|
142
|
+
const { config, validateConfig } = await import('../../lib/config.js');
|
|
143
|
+
|
|
144
|
+
// Empty string is falsy, so it falls back to default
|
|
145
|
+
expect(config.clerkClientId).toBe('nULlnomaKB9rRGP2');
|
|
146
|
+
expect(validateConfig()).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should accept valid HTTPS URL for API_ENDPOINT', async () => {
|
|
150
|
+
process.env.API_ENDPOINT = 'https://api.example.com/v1';
|
|
151
|
+
|
|
152
|
+
const { validateConfig } = await import('../../lib/config.js');
|
|
153
|
+
|
|
154
|
+
expect(validateConfig()).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should accept valid HTTP URL for API_ENDPOINT (development)', async () => {
|
|
158
|
+
process.env.API_ENDPOINT = 'http://localhost:8000';
|
|
159
|
+
|
|
160
|
+
const { validateConfig } = await import('../../lib/config.js');
|
|
161
|
+
|
|
162
|
+
expect(validateConfig()).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for lib/jwt-utils.js
|
|
3
|
+
*
|
|
4
|
+
* Tests JWT parsing, validation, and expiration checking functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
parseJWT,
|
|
10
|
+
isJWTExpired,
|
|
11
|
+
validateJWTStructure,
|
|
12
|
+
getTimeUntilExpiration,
|
|
13
|
+
} from '../../lib/jwt-utils.js';
|
|
14
|
+
|
|
15
|
+
describe('jwt-utils', () => {
|
|
16
|
+
describe('parseJWT', () => {
|
|
17
|
+
it('should parse a valid JWT and return the payload', () => {
|
|
18
|
+
// Create a valid JWT with base64url-encoded payload
|
|
19
|
+
const payload = { sub: 'user_123', email: 'test@test.com', exp: 9999999999 };
|
|
20
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
21
|
+
const token = `header.${encodedPayload}.signature`;
|
|
22
|
+
|
|
23
|
+
const result = parseJWT(token);
|
|
24
|
+
|
|
25
|
+
expect(result).toEqual(payload);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return null for invalid JWT with wrong number of parts', () => {
|
|
29
|
+
expect(parseJWT('invalid')).toBeNull();
|
|
30
|
+
expect(parseJWT('only.two')).toBeNull();
|
|
31
|
+
expect(parseJWT('too.many.parts.here')).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return null for JWT with invalid base64 payload', () => {
|
|
35
|
+
const token = 'header.!!!invalid-base64!!!.signature';
|
|
36
|
+
|
|
37
|
+
const result = parseJWT(token);
|
|
38
|
+
|
|
39
|
+
expect(result).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return null for JWT with non-JSON payload', () => {
|
|
43
|
+
const encodedPayload = Buffer.from('not-json').toString('base64');
|
|
44
|
+
const token = `header.${encodedPayload}.signature`;
|
|
45
|
+
|
|
46
|
+
const result = parseJWT(token);
|
|
47
|
+
|
|
48
|
+
expect(result).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return null for empty string', () => {
|
|
52
|
+
expect(parseJWT('')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should handle JWT with special characters in payload', () => {
|
|
56
|
+
const payload = { sub: 'user_123', email: 'test+special@test.com', name: 'Test User' };
|
|
57
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64');
|
|
58
|
+
const token = `header.${encodedPayload}.signature`;
|
|
59
|
+
|
|
60
|
+
const result = parseJWT(token);
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual(payload);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('isJWTExpired', () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
// Mock Date.now() for consistent testing
|
|
69
|
+
vi.useFakeTimers();
|
|
70
|
+
vi.setSystemTime(new Date('2024-06-15T12:00:00.000Z'));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
vi.useRealTimers();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return true for expired token', () => {
|
|
78
|
+
// Token expired on Jan 1, 2024
|
|
79
|
+
const decoded = { exp: Math.floor(new Date('2024-01-01T00:00:00.000Z').getTime() / 1000) };
|
|
80
|
+
|
|
81
|
+
expect(isJWTExpired(decoded)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return false for valid (non-expired) token', () => {
|
|
85
|
+
// Token expires on Dec 31, 2024
|
|
86
|
+
const decoded = { exp: Math.floor(new Date('2024-12-31T00:00:00.000Z').getTime() / 1000) };
|
|
87
|
+
|
|
88
|
+
expect(isJWTExpired(decoded)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return true when token has no exp claim', () => {
|
|
92
|
+
expect(isJWTExpired({})).toBe(true);
|
|
93
|
+
expect(isJWTExpired({ sub: 'user_123' })).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return true for null/undefined decoded value', () => {
|
|
97
|
+
expect(isJWTExpired(null)).toBe(true);
|
|
98
|
+
expect(isJWTExpired(undefined)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should respect buffer parameter', () => {
|
|
102
|
+
// Token expires in exactly 5 minutes from now
|
|
103
|
+
const fiveMinutesFromNow = Math.floor(Date.now() / 1000) + 300;
|
|
104
|
+
const decoded = { exp: fiveMinutesFromNow };
|
|
105
|
+
|
|
106
|
+
// Without buffer, not expired
|
|
107
|
+
expect(isJWTExpired(decoded, 0)).toBe(false);
|
|
108
|
+
|
|
109
|
+
// With 10-minute buffer (600000ms), considered expired
|
|
110
|
+
expect(isJWTExpired(decoded, 600000)).toBe(true);
|
|
111
|
+
|
|
112
|
+
// With 3-minute buffer (180000ms), not expired yet
|
|
113
|
+
expect(isJWTExpired(decoded, 180000)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should handle edge case where exp equals current time', () => {
|
|
117
|
+
const now = Math.floor(Date.now() / 1000);
|
|
118
|
+
const decoded = { exp: now };
|
|
119
|
+
|
|
120
|
+
// Exactly at expiration time should be considered expired
|
|
121
|
+
expect(isJWTExpired(decoded)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('validateJWTStructure', () => {
|
|
126
|
+
it('should return truthy for valid JWT structure with sub and exp', () => {
|
|
127
|
+
const decoded = { sub: 'user_123', exp: 9999999999 };
|
|
128
|
+
|
|
129
|
+
expect(validateJWTStructure(decoded)).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return falsy when sub is missing', () => {
|
|
133
|
+
const decoded = { exp: 9999999999, email: 'test@test.com' };
|
|
134
|
+
|
|
135
|
+
expect(validateJWTStructure(decoded)).toBeFalsy();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return falsy when exp is missing', () => {
|
|
139
|
+
const decoded = { sub: 'user_123', email: 'test@test.com' };
|
|
140
|
+
|
|
141
|
+
expect(validateJWTStructure(decoded)).toBeFalsy();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should return falsy for null/undefined', () => {
|
|
145
|
+
expect(validateJWTStructure(null)).toBeFalsy();
|
|
146
|
+
expect(validateJWTStructure(undefined)).toBeFalsy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return falsy for non-object values', () => {
|
|
150
|
+
expect(validateJWTStructure('string')).toBeFalsy();
|
|
151
|
+
expect(validateJWTStructure(123)).toBeFalsy();
|
|
152
|
+
expect(validateJWTStructure([])).toBeFalsy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return truthy when sub and exp have any truthy value', () => {
|
|
156
|
+
const decoded = { sub: 'any_value', exp: 1 };
|
|
157
|
+
|
|
158
|
+
expect(validateJWTStructure(decoded)).toBeTruthy();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return falsy when sub or exp are falsy (0, empty string)', () => {
|
|
162
|
+
expect(validateJWTStructure({ sub: '', exp: 9999999999 })).toBeFalsy();
|
|
163
|
+
expect(validateJWTStructure({ sub: 'user_123', exp: 0 })).toBeFalsy();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('getTimeUntilExpiration', () => {
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
vi.useFakeTimers();
|
|
170
|
+
vi.setSystemTime(new Date('2024-06-15T12:00:00.000Z'));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterEach(() => {
|
|
174
|
+
vi.useRealTimers();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return positive milliseconds for future expiration', () => {
|
|
178
|
+
// Expires 1 hour from now
|
|
179
|
+
const oneHourFromNow = Math.floor(Date.now() / 1000) + 3600;
|
|
180
|
+
const decoded = { exp: oneHourFromNow };
|
|
181
|
+
|
|
182
|
+
const result = getTimeUntilExpiration(decoded);
|
|
183
|
+
|
|
184
|
+
// Should be approximately 1 hour in milliseconds
|
|
185
|
+
expect(result).toBeCloseTo(3600000, -2); // Allow small variance
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should return negative milliseconds for past expiration', () => {
|
|
189
|
+
// Expired 1 hour ago
|
|
190
|
+
const oneHourAgo = Math.floor(Date.now() / 1000) - 3600;
|
|
191
|
+
const decoded = { exp: oneHourAgo };
|
|
192
|
+
|
|
193
|
+
const result = getTimeUntilExpiration(decoded);
|
|
194
|
+
|
|
195
|
+
expect(result).toBeCloseTo(-3600000, -2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return -1 when no exp claim exists', () => {
|
|
199
|
+
expect(getTimeUntilExpiration({})).toBe(-1);
|
|
200
|
+
expect(getTimeUntilExpiration({ sub: 'user_123' })).toBe(-1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should return -1 for null/undefined decoded value', () => {
|
|
204
|
+
expect(getTimeUntilExpiration(null)).toBe(-1);
|
|
205
|
+
expect(getTimeUntilExpiration(undefined)).toBe(-1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should return approximately 0 when exp equals current time', () => {
|
|
209
|
+
const now = Math.floor(Date.now() / 1000);
|
|
210
|
+
const decoded = { exp: now };
|
|
211
|
+
|
|
212
|
+
const result = getTimeUntilExpiration(decoded);
|
|
213
|
+
|
|
214
|
+
expect(result).toBeCloseTo(0, -2);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|