@l4yercak3/cli 1.0.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/.claude/settings.local.json +18 -0
- package/.cursor/rules.md +203 -0
- package/.eslintrc.js +31 -0
- package/README.md +227 -0
- package/bin/cli.js +61 -0
- package/docs/ADDING_NEW_PROJECT_TYPE.md +156 -0
- package/docs/ARCHITECTURE_RELATIONSHIPS.md +411 -0
- package/docs/CLI_AUTHENTICATION.md +214 -0
- package/docs/DETECTOR_ARCHITECTURE.md +326 -0
- package/docs/DEVELOPMENT.md +194 -0
- package/docs/IMPLEMENTATION_PHASES.md +468 -0
- package/docs/OAUTH_CLARIFICATION.md +258 -0
- package/docs/OAUTH_SETUP_GUIDE_TEMPLATE.md +211 -0
- package/docs/PHASE_0_PROGRESS.md +120 -0
- package/docs/PHASE_1_COMPLETE.md +366 -0
- package/docs/PHASE_SUMMARY.md +149 -0
- package/docs/PLAN.md +511 -0
- package/docs/README.md +56 -0
- package/docs/STRIPE_INTEGRATION.md +447 -0
- package/docs/SUMMARY.md +230 -0
- package/docs/UPDATED_PLAN.md +447 -0
- package/package.json +53 -0
- package/src/api/backend-client.js +148 -0
- package/src/commands/login.js +146 -0
- package/src/commands/logout.js +24 -0
- package/src/commands/spread.js +364 -0
- package/src/commands/status.js +62 -0
- package/src/config/config-manager.js +205 -0
- package/src/detectors/api-client-detector.js +85 -0
- package/src/detectors/base-detector.js +77 -0
- package/src/detectors/github-detector.js +74 -0
- package/src/detectors/index.js +80 -0
- package/src/detectors/nextjs-detector.js +139 -0
- package/src/detectors/oauth-detector.js +122 -0
- package/src/detectors/registry.js +97 -0
- package/src/generators/api-client-generator.js +197 -0
- package/src/generators/env-generator.js +162 -0
- package/src/generators/gitignore-generator.js +92 -0
- package/src/generators/index.js +50 -0
- package/src/generators/nextauth-generator.js +242 -0
- package/src/generators/oauth-guide-generator.js +277 -0
- package/src/logo.js +116 -0
- package/tests/api-client-detector.test.js +214 -0
- package/tests/api-client-generator.test.js +169 -0
- package/tests/backend-client.test.js +361 -0
- package/tests/base-detector.test.js +101 -0
- package/tests/commands/login.test.js +98 -0
- package/tests/commands/logout.test.js +70 -0
- package/tests/commands/status.test.js +167 -0
- package/tests/config-manager.test.js +313 -0
- package/tests/detector-index.test.js +209 -0
- package/tests/detector-registry.test.js +93 -0
- package/tests/env-generator.test.js +278 -0
- package/tests/generators-index.test.js +215 -0
- package/tests/github-detector.test.js +145 -0
- package/tests/gitignore-generator.test.js +109 -0
- package/tests/logo.test.js +96 -0
- package/tests/nextauth-generator.test.js +231 -0
- package/tests/nextjs-detector.test.js +235 -0
- package/tests/oauth-detector.test.js +264 -0
- package/tests/oauth-guide-generator.test.js +273 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Backend Client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('node-fetch');
|
|
6
|
+
jest.mock('../src/config/config-manager');
|
|
7
|
+
|
|
8
|
+
const fetch = require('node-fetch');
|
|
9
|
+
const configManager = require('../src/config/config-manager');
|
|
10
|
+
|
|
11
|
+
// Set up mock before requiring BackendClient
|
|
12
|
+
configManager.getBackendUrl.mockReturnValue('https://backend.test.com');
|
|
13
|
+
|
|
14
|
+
// Need to require after mocking
|
|
15
|
+
const BackendClient = require('../src/api/backend-client');
|
|
16
|
+
|
|
17
|
+
describe('BackendClient', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
configManager.getBackendUrl.mockReturnValue('https://backend.test.com');
|
|
21
|
+
configManager.getSession.mockReturnValue(null);
|
|
22
|
+
// Reset baseUrl since the module was already instantiated
|
|
23
|
+
BackendClient.baseUrl = 'https://backend.test.com';
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('getHeaders', () => {
|
|
27
|
+
it('returns Content-Type header when no session', () => {
|
|
28
|
+
configManager.getSession.mockReturnValue(null);
|
|
29
|
+
|
|
30
|
+
const headers = BackendClient.getHeaders();
|
|
31
|
+
|
|
32
|
+
expect(headers['Content-Type']).toBe('application/json');
|
|
33
|
+
expect(headers['Authorization']).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('includes Authorization header when session exists', () => {
|
|
37
|
+
configManager.getSession.mockReturnValue({ token: 'test-token-123' });
|
|
38
|
+
|
|
39
|
+
const headers = BackendClient.getHeaders();
|
|
40
|
+
|
|
41
|
+
expect(headers['Authorization']).toBe('Bearer test-token-123');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('does not include Authorization when session has no token', () => {
|
|
45
|
+
configManager.getSession.mockReturnValue({ expiresAt: Date.now() });
|
|
46
|
+
|
|
47
|
+
const headers = BackendClient.getHeaders();
|
|
48
|
+
|
|
49
|
+
expect(headers['Authorization']).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('request', () => {
|
|
54
|
+
it('makes GET request without body', async () => {
|
|
55
|
+
const mockResponse = {
|
|
56
|
+
ok: true,
|
|
57
|
+
json: jest.fn().mockResolvedValue({ data: 'test' }),
|
|
58
|
+
};
|
|
59
|
+
fetch.mockResolvedValue(mockResponse);
|
|
60
|
+
|
|
61
|
+
const result = await BackendClient.request('GET', '/api/test');
|
|
62
|
+
|
|
63
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
64
|
+
'https://backend.test.com/api/test',
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
method: 'GET',
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
expect(result).toEqual({ data: 'test' });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('makes POST request with body', async () => {
|
|
73
|
+
const mockResponse = {
|
|
74
|
+
ok: true,
|
|
75
|
+
json: jest.fn().mockResolvedValue({ success: true }),
|
|
76
|
+
};
|
|
77
|
+
fetch.mockResolvedValue(mockResponse);
|
|
78
|
+
|
|
79
|
+
await BackendClient.request('POST', '/api/test', { name: 'test' });
|
|
80
|
+
|
|
81
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
82
|
+
'https://backend.test.com/api/test',
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: JSON.stringify({ name: 'test' }),
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('makes PUT request with body', async () => {
|
|
91
|
+
const mockResponse = {
|
|
92
|
+
ok: true,
|
|
93
|
+
json: jest.fn().mockResolvedValue({ success: true }),
|
|
94
|
+
};
|
|
95
|
+
fetch.mockResolvedValue(mockResponse);
|
|
96
|
+
|
|
97
|
+
await BackendClient.request('PUT', '/api/test', { name: 'updated' });
|
|
98
|
+
|
|
99
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
100
|
+
expect.any(String),
|
|
101
|
+
expect.objectContaining({
|
|
102
|
+
method: 'PUT',
|
|
103
|
+
body: JSON.stringify({ name: 'updated' }),
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('makes PATCH request with body', async () => {
|
|
109
|
+
const mockResponse = {
|
|
110
|
+
ok: true,
|
|
111
|
+
json: jest.fn().mockResolvedValue({ success: true }),
|
|
112
|
+
};
|
|
113
|
+
fetch.mockResolvedValue(mockResponse);
|
|
114
|
+
|
|
115
|
+
await BackendClient.request('PATCH', '/api/test', { name: 'patched' });
|
|
116
|
+
|
|
117
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
118
|
+
expect.any(String),
|
|
119
|
+
expect.objectContaining({
|
|
120
|
+
method: 'PATCH',
|
|
121
|
+
body: JSON.stringify({ name: 'patched' }),
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not include body for DELETE request', async () => {
|
|
127
|
+
const mockResponse = {
|
|
128
|
+
ok: true,
|
|
129
|
+
json: jest.fn().mockResolvedValue({ deleted: true }),
|
|
130
|
+
};
|
|
131
|
+
fetch.mockResolvedValue(mockResponse);
|
|
132
|
+
|
|
133
|
+
await BackendClient.request('DELETE', '/api/test', { id: '123' });
|
|
134
|
+
|
|
135
|
+
const fetchCall = fetch.mock.calls[0][1];
|
|
136
|
+
expect(fetchCall.body).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('throws error on non-ok response', async () => {
|
|
140
|
+
const mockResponse = {
|
|
141
|
+
ok: false,
|
|
142
|
+
status: 401,
|
|
143
|
+
json: jest.fn().mockResolvedValue({ message: 'Unauthorized' }),
|
|
144
|
+
};
|
|
145
|
+
fetch.mockResolvedValue(mockResponse);
|
|
146
|
+
|
|
147
|
+
await expect(BackendClient.request('GET', '/api/test')).rejects.toThrow('Unauthorized');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('throws generic error when no message in response', async () => {
|
|
151
|
+
const mockResponse = {
|
|
152
|
+
ok: false,
|
|
153
|
+
status: 500,
|
|
154
|
+
json: jest.fn().mockResolvedValue({}),
|
|
155
|
+
};
|
|
156
|
+
fetch.mockResolvedValue(mockResponse);
|
|
157
|
+
|
|
158
|
+
await expect(BackendClient.request('GET', '/api/test')).rejects.toThrow('API request failed: 500');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('throws network error on fetch failure', async () => {
|
|
162
|
+
fetch.mockRejectedValue(new Error('fetch failed: network error'));
|
|
163
|
+
|
|
164
|
+
await expect(BackendClient.request('GET', '/api/test')).rejects.toThrow(
|
|
165
|
+
'Network error: Could not connect to backend'
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('rethrows non-fetch errors', async () => {
|
|
170
|
+
fetch.mockRejectedValue(new Error('Something else went wrong'));
|
|
171
|
+
|
|
172
|
+
await expect(BackendClient.request('GET', '/api/test')).rejects.toThrow(
|
|
173
|
+
'Something else went wrong'
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('validateSession', () => {
|
|
179
|
+
it('returns response on success', async () => {
|
|
180
|
+
const mockResponse = {
|
|
181
|
+
ok: true,
|
|
182
|
+
json: jest.fn().mockResolvedValue({ valid: true, userId: '123' }),
|
|
183
|
+
};
|
|
184
|
+
fetch.mockResolvedValue(mockResponse);
|
|
185
|
+
|
|
186
|
+
const result = await BackendClient.validateSession();
|
|
187
|
+
|
|
188
|
+
expect(result).toEqual({ valid: true, userId: '123' });
|
|
189
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
190
|
+
expect.stringContaining('/api/v1/auth/cli/validate'),
|
|
191
|
+
expect.any(Object)
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns null on error', async () => {
|
|
196
|
+
fetch.mockRejectedValue(new Error('Network error'));
|
|
197
|
+
|
|
198
|
+
const result = await BackendClient.validateSession();
|
|
199
|
+
|
|
200
|
+
expect(result).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('refreshSession', () => {
|
|
205
|
+
it('refreshes and updates session', async () => {
|
|
206
|
+
configManager.getSession.mockReturnValue({ token: 'old-token' });
|
|
207
|
+
const mockResponse = {
|
|
208
|
+
ok: true,
|
|
209
|
+
json: jest.fn().mockResolvedValue({
|
|
210
|
+
token: 'new-token',
|
|
211
|
+
expiresAt: Date.now() + 3600000,
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
fetch.mockResolvedValue(mockResponse);
|
|
215
|
+
|
|
216
|
+
const result = await BackendClient.refreshSession();
|
|
217
|
+
|
|
218
|
+
expect(result.token).toBe('new-token');
|
|
219
|
+
expect(configManager.saveSession).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({ token: 'new-token' })
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('throws error when no session exists', async () => {
|
|
225
|
+
configManager.getSession.mockReturnValue(null);
|
|
226
|
+
|
|
227
|
+
await expect(BackendClient.refreshSession()).rejects.toThrow('No session to refresh');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('throws error when session has no token', async () => {
|
|
231
|
+
configManager.getSession.mockReturnValue({ expiresAt: Date.now() });
|
|
232
|
+
|
|
233
|
+
await expect(BackendClient.refreshSession()).rejects.toThrow('No session to refresh');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('clears session on refresh failure', async () => {
|
|
237
|
+
configManager.getSession.mockReturnValue({ token: 'old-token' });
|
|
238
|
+
fetch.mockRejectedValue(new Error('Refresh failed'));
|
|
239
|
+
|
|
240
|
+
await expect(BackendClient.refreshSession()).rejects.toThrow('Refresh failed');
|
|
241
|
+
expect(configManager.clearSession).toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('getLoginUrl', () => {
|
|
246
|
+
it('returns provider selection URL when no provider specified', () => {
|
|
247
|
+
const url = BackendClient.getLoginUrl();
|
|
248
|
+
|
|
249
|
+
expect(url).toContain('https://backend.test.com');
|
|
250
|
+
expect(url).toContain('/auth/cli-login');
|
|
251
|
+
expect(url).toContain('callback=');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns direct OAuth URL when provider specified', () => {
|
|
255
|
+
const url = BackendClient.getLoginUrl('google');
|
|
256
|
+
|
|
257
|
+
expect(url).toContain('/api/auth/oauth-signup');
|
|
258
|
+
expect(url).toContain('provider=google');
|
|
259
|
+
expect(url).toContain('sessionType=cli');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('includes encoded callback URL', () => {
|
|
263
|
+
const url = BackendClient.getLoginUrl('github');
|
|
264
|
+
|
|
265
|
+
expect(url).toContain(encodeURIComponent('http://localhost:3001/callback'));
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('generateApiKey', () => {
|
|
270
|
+
it('calls API to generate key', async () => {
|
|
271
|
+
const mockResponse = {
|
|
272
|
+
ok: true,
|
|
273
|
+
json: jest.fn().mockResolvedValue({
|
|
274
|
+
key: 'new-api-key',
|
|
275
|
+
id: 'key-123',
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
fetch.mockResolvedValue(mockResponse);
|
|
279
|
+
|
|
280
|
+
const result = await BackendClient.generateApiKey('org-123', 'My Key', ['read', 'write']);
|
|
281
|
+
|
|
282
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
283
|
+
expect.stringContaining('/api/v1/api-keys/generate'),
|
|
284
|
+
expect.objectContaining({
|
|
285
|
+
method: 'POST',
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
organizationId: 'org-123',
|
|
288
|
+
name: 'My Key',
|
|
289
|
+
scopes: ['read', 'write'],
|
|
290
|
+
}),
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
expect(result.key).toBe('new-api-key');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('uses default scopes when not provided', async () => {
|
|
297
|
+
const mockResponse = {
|
|
298
|
+
ok: true,
|
|
299
|
+
json: jest.fn().mockResolvedValue({ key: 'key' }),
|
|
300
|
+
};
|
|
301
|
+
fetch.mockResolvedValue(mockResponse);
|
|
302
|
+
|
|
303
|
+
await BackendClient.generateApiKey('org-123', 'My Key');
|
|
304
|
+
|
|
305
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
306
|
+
expect.any(String),
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
organizationId: 'org-123',
|
|
310
|
+
name: 'My Key',
|
|
311
|
+
scopes: ['*'],
|
|
312
|
+
}),
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('getOrganizations', () => {
|
|
319
|
+
it('fetches organizations list', async () => {
|
|
320
|
+
const mockResponse = {
|
|
321
|
+
ok: true,
|
|
322
|
+
json: jest.fn().mockResolvedValue({
|
|
323
|
+
organizations: [{ id: '1', name: 'Org 1' }],
|
|
324
|
+
}),
|
|
325
|
+
};
|
|
326
|
+
fetch.mockResolvedValue(mockResponse);
|
|
327
|
+
|
|
328
|
+
const result = await BackendClient.getOrganizations();
|
|
329
|
+
|
|
330
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
331
|
+
expect.stringContaining('/api/v1/organizations'),
|
|
332
|
+
expect.objectContaining({ method: 'GET' })
|
|
333
|
+
);
|
|
334
|
+
expect(result.organizations).toHaveLength(1);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('createOrganization', () => {
|
|
339
|
+
it('creates organization with name', async () => {
|
|
340
|
+
const mockResponse = {
|
|
341
|
+
ok: true,
|
|
342
|
+
json: jest.fn().mockResolvedValue({
|
|
343
|
+
id: 'new-org-123',
|
|
344
|
+
name: 'New Org',
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
347
|
+
fetch.mockResolvedValue(mockResponse);
|
|
348
|
+
|
|
349
|
+
const result = await BackendClient.createOrganization('New Org');
|
|
350
|
+
|
|
351
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
352
|
+
expect.stringContaining('/api/v1/organizations'),
|
|
353
|
+
expect.objectContaining({
|
|
354
|
+
method: 'POST',
|
|
355
|
+
body: JSON.stringify({ name: 'New Org' }),
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
expect(result.name).toBe('New Org');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Base Detector
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const BaseDetector = require('../src/detectors/base-detector');
|
|
6
|
+
|
|
7
|
+
describe('BaseDetector', () => {
|
|
8
|
+
let detector;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
detector = new BaseDetector();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('name getter', () => {
|
|
15
|
+
it('throws error when not overridden', () => {
|
|
16
|
+
expect(() => detector.name).toThrow('Detector must implement name getter');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('priority getter', () => {
|
|
21
|
+
it('returns default priority of 50', () => {
|
|
22
|
+
expect(detector.priority).toBe(50);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('detect', () => {
|
|
27
|
+
it('throws error when not overridden', () => {
|
|
28
|
+
expect(() => detector.detect()).toThrow('Detector must implement detect() method');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('throws error with custom path', () => {
|
|
32
|
+
expect(() => detector.detect('/custom/path')).toThrow('Detector must implement detect() method');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('getSupportedFeatures', () => {
|
|
37
|
+
it('returns default features (all false)', () => {
|
|
38
|
+
const features = detector.getSupportedFeatures();
|
|
39
|
+
|
|
40
|
+
expect(features).toEqual({
|
|
41
|
+
oauth: false,
|
|
42
|
+
stripe: false,
|
|
43
|
+
crm: false,
|
|
44
|
+
projects: false,
|
|
45
|
+
invoices: false,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('getAvailableGenerators', () => {
|
|
51
|
+
it('returns default generators', () => {
|
|
52
|
+
const generators = detector.getAvailableGenerators();
|
|
53
|
+
|
|
54
|
+
expect(generators).toEqual(['api-client', 'env']);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('inheritance', () => {
|
|
59
|
+
it('can be extended with custom implementation', () => {
|
|
60
|
+
class CustomDetector extends BaseDetector {
|
|
61
|
+
get name() {
|
|
62
|
+
return 'custom';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get priority() {
|
|
66
|
+
return 75;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
detect(projectPath) {
|
|
70
|
+
return {
|
|
71
|
+
detected: true,
|
|
72
|
+
confidence: 0.9,
|
|
73
|
+
metadata: { path: projectPath },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getSupportedFeatures() {
|
|
78
|
+
return {
|
|
79
|
+
oauth: true,
|
|
80
|
+
stripe: true,
|
|
81
|
+
crm: false,
|
|
82
|
+
projects: false,
|
|
83
|
+
invoices: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getAvailableGenerators() {
|
|
88
|
+
return ['api-client', 'env', 'custom-generator'];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const customDetector = new CustomDetector();
|
|
93
|
+
|
|
94
|
+
expect(customDetector.name).toBe('custom');
|
|
95
|
+
expect(customDetector.priority).toBe(75);
|
|
96
|
+
expect(customDetector.detect('/test').detected).toBe(true);
|
|
97
|
+
expect(customDetector.getSupportedFeatures().oauth).toBe(true);
|
|
98
|
+
expect(customDetector.getAvailableGenerators()).toContain('custom-generator');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Login Command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('open', () => ({
|
|
6
|
+
default: jest.fn().mockResolvedValue(undefined),
|
|
7
|
+
}));
|
|
8
|
+
jest.mock('../../src/config/config-manager');
|
|
9
|
+
jest.mock('../../src/api/backend-client');
|
|
10
|
+
jest.mock('chalk', () => ({
|
|
11
|
+
cyan: (str) => str,
|
|
12
|
+
yellow: (str) => str,
|
|
13
|
+
green: (str) => str,
|
|
14
|
+
gray: (str) => str,
|
|
15
|
+
red: (str) => str,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const configManager = require('../../src/config/config-manager');
|
|
19
|
+
const backendClient = require('../../src/api/backend-client');
|
|
20
|
+
const { default: open } = require('open');
|
|
21
|
+
|
|
22
|
+
// Can't easily test the full flow with HTTP server, so test module exports
|
|
23
|
+
const loginCommand = require('../../src/commands/login');
|
|
24
|
+
|
|
25
|
+
describe('Login Command', () => {
|
|
26
|
+
let consoleOutput = [];
|
|
27
|
+
let consoleErrors = [];
|
|
28
|
+
const originalConsoleLog = console.log;
|
|
29
|
+
const originalConsoleError = console.error;
|
|
30
|
+
const originalProcessExit = process.exit;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
consoleOutput = [];
|
|
35
|
+
consoleErrors = [];
|
|
36
|
+
console.log = jest.fn((...args) => {
|
|
37
|
+
consoleOutput.push(args.join(' '));
|
|
38
|
+
});
|
|
39
|
+
console.error = jest.fn((...args) => {
|
|
40
|
+
consoleErrors.push(args.join(' '));
|
|
41
|
+
});
|
|
42
|
+
process.exit = jest.fn();
|
|
43
|
+
|
|
44
|
+
configManager.getSession.mockReturnValue(null);
|
|
45
|
+
backendClient.getLoginUrl.mockReturnValue('https://backend.test.com/auth/cli-login');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
console.log = originalConsoleLog;
|
|
50
|
+
console.error = originalConsoleError;
|
|
51
|
+
process.exit = originalProcessExit;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('module exports', () => {
|
|
55
|
+
it('exports command name', () => {
|
|
56
|
+
expect(loginCommand.command).toBe('login');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('exports description', () => {
|
|
60
|
+
expect(loginCommand.description).toBe('Authenticate with L4YERCAK3 platform');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('exports handler function', () => {
|
|
64
|
+
expect(typeof loginCommand.handler).toBe('function');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('handler - already logged in', () => {
|
|
69
|
+
it('shows warning when already logged in', async () => {
|
|
70
|
+
configManager.isLoggedIn.mockReturnValue(true);
|
|
71
|
+
configManager.getSession.mockReturnValue({
|
|
72
|
+
email: 'user@example.com',
|
|
73
|
+
expiresAt: Date.now() + 3600000,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await loginCommand.handler();
|
|
77
|
+
|
|
78
|
+
expect(consoleOutput.some((line) => line.includes('already logged in'))).toBe(true);
|
|
79
|
+
expect(consoleOutput.some((line) => line.includes('user@example.com'))).toBe(true);
|
|
80
|
+
expect(open).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('suggests logout when already logged in', async () => {
|
|
84
|
+
configManager.isLoggedIn.mockReturnValue(true);
|
|
85
|
+
configManager.getSession.mockReturnValue({
|
|
86
|
+
email: 'user@example.com',
|
|
87
|
+
expiresAt: Date.now() + 3600000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await loginCommand.handler();
|
|
91
|
+
|
|
92
|
+
expect(consoleOutput.some((line) => line.includes('logout'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Note: Full login flow testing is complex due to HTTP server
|
|
97
|
+
// These tests verify the basic structure and early-exit paths
|
|
98
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Logout Command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
jest.mock('../../src/config/config-manager');
|
|
6
|
+
jest.mock('chalk', () => ({
|
|
7
|
+
yellow: (str) => str,
|
|
8
|
+
green: (str) => str,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const configManager = require('../../src/config/config-manager');
|
|
12
|
+
const logoutCommand = require('../../src/commands/logout');
|
|
13
|
+
|
|
14
|
+
describe('Logout Command', () => {
|
|
15
|
+
let consoleOutput = [];
|
|
16
|
+
const originalConsoleLog = console.log;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
consoleOutput = [];
|
|
21
|
+
console.log = jest.fn((...args) => {
|
|
22
|
+
consoleOutput.push(args.join(' '));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
console.log = originalConsoleLog;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('module exports', () => {
|
|
31
|
+
it('exports command name', () => {
|
|
32
|
+
expect(logoutCommand.command).toBe('logout');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('exports description', () => {
|
|
36
|
+
expect(logoutCommand.description).toBe('Log out from L4YERCAK3 platform');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('exports handler function', () => {
|
|
40
|
+
expect(typeof logoutCommand.handler).toBe('function');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('handler', () => {
|
|
45
|
+
it('shows warning when not logged in', async () => {
|
|
46
|
+
configManager.isLoggedIn.mockReturnValue(false);
|
|
47
|
+
|
|
48
|
+
await logoutCommand.handler();
|
|
49
|
+
|
|
50
|
+
expect(consoleOutput.some((line) => line.includes('not logged in'))).toBe(true);
|
|
51
|
+
expect(configManager.clearSession).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('clears session when logged in', async () => {
|
|
55
|
+
configManager.isLoggedIn.mockReturnValue(true);
|
|
56
|
+
|
|
57
|
+
await logoutCommand.handler();
|
|
58
|
+
|
|
59
|
+
expect(configManager.clearSession).toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('shows success message after logout', async () => {
|
|
63
|
+
configManager.isLoggedIn.mockReturnValue(true);
|
|
64
|
+
|
|
65
|
+
await logoutCommand.handler();
|
|
66
|
+
|
|
67
|
+
expect(consoleOutput.some((line) => line.includes('Successfully logged out'))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|