@unifiedmemory/cli 1.3.1 → 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/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,92 @@
|
|
|
1
|
+
name: Test and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [18, 20, 22]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
20
|
+
uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: ${{ matrix.node-version }}
|
|
23
|
+
cache: 'npm'
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: npm run test:ci
|
|
30
|
+
env:
|
|
31
|
+
CI: true
|
|
32
|
+
|
|
33
|
+
- name: Upload test results
|
|
34
|
+
uses: actions/upload-artifact@v4
|
|
35
|
+
if: always()
|
|
36
|
+
with:
|
|
37
|
+
name: test-results-node-${{ matrix.node-version }}
|
|
38
|
+
path: test-results.xml
|
|
39
|
+
|
|
40
|
+
publish:
|
|
41
|
+
needs: test
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
44
|
+
permissions:
|
|
45
|
+
contents: write
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Setup Node.js
|
|
51
|
+
uses: actions/setup-node@v4
|
|
52
|
+
with:
|
|
53
|
+
node-version: 24
|
|
54
|
+
registry-url: 'https://registry.npmjs.org'
|
|
55
|
+
cache: 'npm'
|
|
56
|
+
|
|
57
|
+
- name: Install dependencies
|
|
58
|
+
run: npm ci
|
|
59
|
+
|
|
60
|
+
- name: Check if version changed
|
|
61
|
+
id: version-check
|
|
62
|
+
run: |
|
|
63
|
+
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
64
|
+
NPM_VERSION=$(npm view @unifiedmemory/cli version 2>/dev/null || echo "0.0.0")
|
|
65
|
+
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
|
66
|
+
echo "published=$NPM_VERSION" >> $GITHUB_OUTPUT
|
|
67
|
+
if [ "$CURRENT_VERSION" != "$NPM_VERSION" ]; then
|
|
68
|
+
echo "version_changed=true" >> $GITHUB_OUTPUT
|
|
69
|
+
echo "✅ Version changed: $NPM_VERSION → $CURRENT_VERSION"
|
|
70
|
+
else
|
|
71
|
+
echo "version_changed=false" >> $GITHUB_OUTPUT
|
|
72
|
+
echo "ℹ️ Version unchanged ($CURRENT_VERSION) - skipping publish"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
- name: Publish to npm
|
|
76
|
+
if: steps.version-check.outputs.version_changed == 'true'
|
|
77
|
+
run: npm publish --access public
|
|
78
|
+
env:
|
|
79
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
80
|
+
|
|
81
|
+
- name: Create Git tag
|
|
82
|
+
if: steps.version-check.outputs.version_changed == 'true'
|
|
83
|
+
run: |
|
|
84
|
+
git config user.name "github-actions[bot]"
|
|
85
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
86
|
+
git tag v${{ steps.version-check.outputs.current }}
|
|
87
|
+
git push origin v${{ steps.version-check.outputs.current }}
|
|
88
|
+
|
|
89
|
+
- name: Skip publish notification
|
|
90
|
+
if: steps.version-check.outputs.version_changed == 'false'
|
|
91
|
+
run: |
|
|
92
|
+
echo "::notice::Skipped npm publish - version ${{ steps.version-check.outputs.current }} already published"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unifiedmemory/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "UnifiedMemory CLI - AI code assistant integration",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node index.js",
|
|
12
|
-
"build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um"
|
|
12
|
+
"build": "pkg . --targets node18-linux-x64,node18-macos-x64,node18-win-x64 --output dist/um",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"test:unit": "vitest run tests/unit",
|
|
17
|
+
"test:integration": "vitest run tests/integration",
|
|
18
|
+
"test:ci": "vitest run --reporter=junit --outputFile=test-results.xml"
|
|
13
19
|
},
|
|
14
20
|
"keywords": [
|
|
15
21
|
"ai",
|
|
@@ -42,10 +48,16 @@
|
|
|
42
48
|
"open": "^10.0.3"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
45
|
-
"
|
|
51
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
52
|
+
"msw": "^2.7.0",
|
|
53
|
+
"pkg": "^5.8.1",
|
|
54
|
+
"vitest": "^3.0.0"
|
|
46
55
|
},
|
|
47
56
|
"repository": {
|
|
48
57
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/
|
|
58
|
+
"url": "https://github.com/Episodic-Solutions/um-cli.git"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
50
62
|
}
|
|
51
63
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"accessToken": "mock_expired_access_token",
|
|
3
|
+
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6MTYwMDAwMDAwMCwiaWF0IjoxNTk5OTk2NDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSJ9.mock_signature",
|
|
4
|
+
"refresh_token": "mock_refresh_token_for_expired",
|
|
5
|
+
"tokenType": "Bearer",
|
|
6
|
+
"expiresIn": 3600,
|
|
7
|
+
"receivedAt": 1599996400000,
|
|
8
|
+
"decoded": {
|
|
9
|
+
"sub": "user_123456789",
|
|
10
|
+
"email": "test@test.com",
|
|
11
|
+
"exp": 1600000000,
|
|
12
|
+
"iat": 1599996400,
|
|
13
|
+
"sid": "sess_123456789"
|
|
14
|
+
},
|
|
15
|
+
"selectedOrg": {
|
|
16
|
+
"id": "org_456789012",
|
|
17
|
+
"name": "Test Organization",
|
|
18
|
+
"role": "admin"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"accessToken": "mock_access_token_abc123",
|
|
3
|
+
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQ1Njc4OSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImV4cCI6OTk5OTk5OTk5OSwiaWF0IjoxNjAwMDAwMDAwLCJzaWQiOiJzZXNzXzEyMzQ1Njc4OSIsIm9yZ19pZCI6Im9yZ180NTY3ODkwMTIifQ.mock_signature",
|
|
4
|
+
"refresh_token": "mock_refresh_token_xyz789",
|
|
5
|
+
"tokenType": "Bearer",
|
|
6
|
+
"expiresIn": 3600,
|
|
7
|
+
"receivedAt": 1700000000000,
|
|
8
|
+
"decoded": {
|
|
9
|
+
"sub": "user_123456789",
|
|
10
|
+
"email": "test@test.com",
|
|
11
|
+
"exp": 9999999999,
|
|
12
|
+
"iat": 1600000000,
|
|
13
|
+
"sid": "sess_123456789",
|
|
14
|
+
"org_id": "org_456789012"
|
|
15
|
+
},
|
|
16
|
+
"selectedOrg": {
|
|
17
|
+
"id": "org_456789012",
|
|
18
|
+
"name": "Test Organization",
|
|
19
|
+
"role": "admin"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0",
|
|
3
|
+
"org_id": "org_456789012",
|
|
4
|
+
"project_id": "proj_987654321",
|
|
5
|
+
"project_name": "test-project",
|
|
6
|
+
"api_url": "https://api.test.unifiedmemory.ai",
|
|
7
|
+
"created_at": "2024-01-01T00:00:00.000Z",
|
|
8
|
+
"mcp_config": {
|
|
9
|
+
"inject_headers": true,
|
|
10
|
+
"auto_context": true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSW (Mock Service Worker) handlers for API mocking
|
|
3
|
+
*
|
|
4
|
+
* These handlers intercept HTTP requests during tests and return
|
|
5
|
+
* predefined responses without hitting real APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { http, HttpResponse } from 'msw';
|
|
9
|
+
|
|
10
|
+
const API_BASE_URL = 'https://rose-asp-main-1c0b114.d2.zuplo.dev';
|
|
11
|
+
|
|
12
|
+
// Sample project data
|
|
13
|
+
const mockProjects = [
|
|
14
|
+
{
|
|
15
|
+
id: 'proj_001',
|
|
16
|
+
name: 'project-one',
|
|
17
|
+
display_name: 'Project One',
|
|
18
|
+
slug: 'project-one',
|
|
19
|
+
description: 'First test project',
|
|
20
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'proj_002',
|
|
24
|
+
name: 'project-two',
|
|
25
|
+
display_name: 'Project Two',
|
|
26
|
+
slug: 'project-two',
|
|
27
|
+
description: 'Second test project',
|
|
28
|
+
created_at: '2024-01-02T00:00:00.000Z',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Sample MCP tools data
|
|
33
|
+
const mockMCPTools = [
|
|
34
|
+
{
|
|
35
|
+
name: 'create_note',
|
|
36
|
+
description: 'Create a new note in the vault',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
content: { type: 'string', description: 'Note content' },
|
|
41
|
+
pathParams: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
org: { type: 'string' },
|
|
45
|
+
proj: { type: 'string' },
|
|
46
|
+
},
|
|
47
|
+
required: ['org', 'proj'],
|
|
48
|
+
},
|
|
49
|
+
headers: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
'X-Org-Id': { type: 'string' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ['content', 'pathParams'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'search_notes',
|
|
61
|
+
description: 'Search notes in the vault',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
query: { type: 'string', description: 'Search query' },
|
|
66
|
+
pathParams: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
org: { type: 'string' },
|
|
70
|
+
proj: { type: 'string' },
|
|
71
|
+
},
|
|
72
|
+
required: ['org', 'proj'],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ['query', 'pathParams'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default API handlers for common endpoints
|
|
82
|
+
*/
|
|
83
|
+
export const handlers = [
|
|
84
|
+
// GET /v1/orgs/:org/projects - List projects
|
|
85
|
+
http.get(`${API_BASE_URL}/v1/orgs/:org/projects`, ({ request, params }) => {
|
|
86
|
+
const authHeader = request.headers.get('Authorization');
|
|
87
|
+
|
|
88
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
89
|
+
return HttpResponse.json(
|
|
90
|
+
{ error: 'Unauthorized', message: 'Missing or invalid authorization header' },
|
|
91
|
+
{ status: 401 }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return HttpResponse.json({ items: mockProjects });
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
// POST /v1/orgs/:org/projects - Create project
|
|
99
|
+
http.post(`${API_BASE_URL}/v1/orgs/:org/projects`, async ({ request, params }) => {
|
|
100
|
+
const authHeader = request.headers.get('Authorization');
|
|
101
|
+
|
|
102
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
103
|
+
return HttpResponse.json(
|
|
104
|
+
{ error: 'Unauthorized', message: 'Missing or invalid authorization header' },
|
|
105
|
+
{ status: 401 }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const body = await request.json();
|
|
110
|
+
|
|
111
|
+
return HttpResponse.json({
|
|
112
|
+
id: `proj_${Date.now()}`,
|
|
113
|
+
name: body.display_name.toLowerCase().replace(/\s+/g, '-'),
|
|
114
|
+
display_name: body.display_name,
|
|
115
|
+
slug: body.display_name.toLowerCase().replace(/\s+/g, '-'),
|
|
116
|
+
description: body.description || '',
|
|
117
|
+
created_at: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
// POST /mcp - MCP endpoint for tools/list
|
|
122
|
+
http.post(`${API_BASE_URL}/mcp`, async ({ request }) => {
|
|
123
|
+
const body = await request.json();
|
|
124
|
+
|
|
125
|
+
if (body.method === 'tools/list') {
|
|
126
|
+
return HttpResponse.json({
|
|
127
|
+
jsonrpc: '2.0',
|
|
128
|
+
id: body.id,
|
|
129
|
+
result: {
|
|
130
|
+
tools: mockMCPTools,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (body.method === 'tools/call') {
|
|
136
|
+
return HttpResponse.json({
|
|
137
|
+
jsonrpc: '2.0',
|
|
138
|
+
id: body.id,
|
|
139
|
+
result: {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Tool ${body.params.name} executed successfully`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return HttpResponse.json(
|
|
151
|
+
{
|
|
152
|
+
jsonrpc: '2.0',
|
|
153
|
+
id: body.id,
|
|
154
|
+
error: { code: -32601, message: 'Method not found' },
|
|
155
|
+
},
|
|
156
|
+
{ status: 400 }
|
|
157
|
+
);
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
// GET /health - Health check
|
|
161
|
+
http.get(`${API_BASE_URL}/health`, () => {
|
|
162
|
+
return HttpResponse.json({ status: 'healthy' });
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Handler that returns 401 for all requests (unauthorized)
|
|
168
|
+
*/
|
|
169
|
+
export const unauthorizedHandlers = [
|
|
170
|
+
http.all(`${API_BASE_URL}/*`, () => {
|
|
171
|
+
return HttpResponse.json(
|
|
172
|
+
{ error: 'Unauthorized', message: 'Token expired or invalid' },
|
|
173
|
+
{ status: 401 }
|
|
174
|
+
);
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Handler that returns 500 for all requests (server error)
|
|
180
|
+
*/
|
|
181
|
+
export const serverErrorHandlers = [
|
|
182
|
+
http.all(`${API_BASE_URL}/*`, () => {
|
|
183
|
+
return HttpResponse.json(
|
|
184
|
+
{ error: 'Internal Server Error', message: 'Something went wrong' },
|
|
185
|
+
{ status: 500 }
|
|
186
|
+
);
|
|
187
|
+
}),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handler that simulates network failure
|
|
192
|
+
*/
|
|
193
|
+
export const networkErrorHandlers = [
|
|
194
|
+
http.all(`${API_BASE_URL}/*`, () => {
|
|
195
|
+
return HttpResponse.error();
|
|
196
|
+
}),
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
// Export mock data for use in tests
|
|
200
|
+
export { mockProjects, mockMCPTools };
|
|
@@ -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
|
+
});
|