tlc-claude-code 1.3.0 → 1.4.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/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('sso-command', () => {
|
|
4
|
+
let SSOCommand;
|
|
5
|
+
let parseArgs;
|
|
6
|
+
let formatProvider;
|
|
7
|
+
let ssoCommand;
|
|
8
|
+
let mockIdPManager;
|
|
9
|
+
let mockRoleMapper;
|
|
10
|
+
let mockSSOSession;
|
|
11
|
+
let mockConfig;
|
|
12
|
+
let mockPrompt;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
// Create mocks
|
|
16
|
+
mockIdPManager = {
|
|
17
|
+
listProviders: vi.fn().mockReturnValue([
|
|
18
|
+
{ name: 'github', type: 'oauth' },
|
|
19
|
+
{ name: 'google', type: 'oauth' },
|
|
20
|
+
{ name: 'okta', type: 'saml' },
|
|
21
|
+
]),
|
|
22
|
+
registerProvider: vi.fn(),
|
|
23
|
+
removeProvider: vi.fn(),
|
|
24
|
+
getProvider: vi.fn().mockReturnValue({
|
|
25
|
+
name: 'github',
|
|
26
|
+
type: 'oauth',
|
|
27
|
+
config: { clientId: 'test-client-id' },
|
|
28
|
+
}),
|
|
29
|
+
handleCallback: vi.fn().mockResolvedValue({
|
|
30
|
+
success: true,
|
|
31
|
+
profile: { email: 'test@example.com' },
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
mockRoleMapper = {
|
|
36
|
+
getMappings: vi.fn().mockReturnValue({
|
|
37
|
+
mappings: [
|
|
38
|
+
{ pattern: 'admins', role: 'admin', priority: 1 },
|
|
39
|
+
{ pattern: 'developers', role: 'engineer', priority: 2 },
|
|
40
|
+
{ pattern: 'qa-team', role: 'qa', priority: 3 },
|
|
41
|
+
],
|
|
42
|
+
defaultRole: 'engineer',
|
|
43
|
+
}),
|
|
44
|
+
validate: vi.fn().mockReturnValue({ valid: true, errors: [] }),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
mockSSOSession = {
|
|
48
|
+
getStatus: vi.fn().mockReturnValue({
|
|
49
|
+
enabled: true,
|
|
50
|
+
activeSessions: 12,
|
|
51
|
+
mfaRequired: true,
|
|
52
|
+
mfaRoles: ['admin'],
|
|
53
|
+
}),
|
|
54
|
+
testProvider: vi.fn().mockResolvedValue({
|
|
55
|
+
success: true,
|
|
56
|
+
message: 'Provider connectivity verified',
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mockConfig = {
|
|
61
|
+
sso: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
roleMappings: [
|
|
64
|
+
{ pattern: 'admins', role: 'admin', priority: 1 },
|
|
65
|
+
{ pattern: 'developers', role: 'engineer', priority: 2 },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
mockPrompt = vi.fn();
|
|
71
|
+
|
|
72
|
+
// Import module
|
|
73
|
+
const module = await import('./sso-command.js');
|
|
74
|
+
SSOCommand = module.SSOCommand;
|
|
75
|
+
parseArgs = module.parseArgs;
|
|
76
|
+
formatProvider = module.formatProvider;
|
|
77
|
+
|
|
78
|
+
// Create instance with mocks
|
|
79
|
+
ssoCommand = new SSOCommand({
|
|
80
|
+
idpManager: mockIdPManager,
|
|
81
|
+
roleMapper: mockRoleMapper,
|
|
82
|
+
ssoSession: mockSSOSession,
|
|
83
|
+
config: mockConfig,
|
|
84
|
+
prompt: mockPrompt,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
vi.resetAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('parseArgs', () => {
|
|
93
|
+
it('parses providers subcommand', () => {
|
|
94
|
+
const result = parseArgs(['providers']);
|
|
95
|
+
expect(result.subcommand).toBe('providers');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('parses add subcommand with provider name', () => {
|
|
99
|
+
const result = parseArgs(['add', 'github']);
|
|
100
|
+
expect(result.subcommand).toBe('add');
|
|
101
|
+
expect(result.provider).toBe('github');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('parses remove subcommand with provider name', () => {
|
|
105
|
+
const result = parseArgs(['remove', 'okta']);
|
|
106
|
+
expect(result.subcommand).toBe('remove');
|
|
107
|
+
expect(result.provider).toBe('okta');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('parses test subcommand with provider name', () => {
|
|
111
|
+
const result = parseArgs(['test', 'google']);
|
|
112
|
+
expect(result.subcommand).toBe('test');
|
|
113
|
+
expect(result.provider).toBe('google');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('parses roles subcommand', () => {
|
|
117
|
+
const result = parseArgs(['roles']);
|
|
118
|
+
expect(result.subcommand).toBe('roles');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('parses status subcommand', () => {
|
|
122
|
+
const result = parseArgs(['status']);
|
|
123
|
+
expect(result.subcommand).toBe('status');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles all subcommands correctly', () => {
|
|
127
|
+
expect(parseArgs(['providers']).subcommand).toBe('providers');
|
|
128
|
+
expect(parseArgs(['add', 'test']).subcommand).toBe('add');
|
|
129
|
+
expect(parseArgs(['remove', 'test']).subcommand).toBe('remove');
|
|
130
|
+
expect(parseArgs(['test', 'test']).subcommand).toBe('test');
|
|
131
|
+
expect(parseArgs(['roles']).subcommand).toBe('roles');
|
|
132
|
+
expect(parseArgs(['status']).subcommand).toBe('status');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('parses --type flag for add', () => {
|
|
136
|
+
const result = parseArgs(['add', 'custom', '--type', 'oauth']);
|
|
137
|
+
expect(result.type).toBe('oauth');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('parses --force flag for remove', () => {
|
|
141
|
+
const result = parseArgs(['remove', 'github', '--force']);
|
|
142
|
+
expect(result.force).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns help subcommand for empty args', () => {
|
|
146
|
+
const result = parseArgs([]);
|
|
147
|
+
expect(result.subcommand).toBe('help');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('execute providers', () => {
|
|
152
|
+
it('lists all providers', async () => {
|
|
153
|
+
const result = await ssoCommand.execute(['providers']);
|
|
154
|
+
|
|
155
|
+
expect(result.success).toBe(true);
|
|
156
|
+
expect(mockIdPManager.listProviders).toHaveBeenCalled();
|
|
157
|
+
expect(result.output).toContain('GitHub');
|
|
158
|
+
expect(result.output).toContain('Google');
|
|
159
|
+
expect(result.output).toContain('Okta');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('shows provider types', async () => {
|
|
163
|
+
const result = await ssoCommand.execute(['providers']);
|
|
164
|
+
|
|
165
|
+
expect(result.output).toContain('OAuth');
|
|
166
|
+
expect(result.output).toContain('SAML');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('shows empty state when no providers', async () => {
|
|
170
|
+
mockIdPManager.listProviders.mockReturnValue([]);
|
|
171
|
+
|
|
172
|
+
const result = await ssoCommand.execute(['providers']);
|
|
173
|
+
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
expect(result.output).toContain('No providers configured');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('shows connection status for providers', async () => {
|
|
179
|
+
mockSSOSession.testProvider.mockResolvedValue({ success: true });
|
|
180
|
+
|
|
181
|
+
const result = await ssoCommand.execute(['providers']);
|
|
182
|
+
|
|
183
|
+
expect(result.output).toMatch(/Connected|Configured/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('execute add', () => {
|
|
188
|
+
it('prompts for OAuth config when type is oauth', async () => {
|
|
189
|
+
mockPrompt
|
|
190
|
+
.mockResolvedValueOnce('oauth') // type
|
|
191
|
+
.mockResolvedValueOnce('client-id-123') // clientId
|
|
192
|
+
.mockResolvedValueOnce('client-secret-456') // clientSecret
|
|
193
|
+
.mockResolvedValueOnce('https://provider.com/auth') // authUrl
|
|
194
|
+
.mockResolvedValueOnce('https://provider.com/token') // tokenUrl
|
|
195
|
+
.mockResolvedValueOnce('https://provider.com/userinfo'); // userInfoUrl
|
|
196
|
+
|
|
197
|
+
const result = await ssoCommand.execute(['add', 'custom-provider']);
|
|
198
|
+
|
|
199
|
+
expect(mockPrompt).toHaveBeenCalled();
|
|
200
|
+
expect(mockIdPManager.registerProvider).toHaveBeenCalledWith(
|
|
201
|
+
'custom-provider',
|
|
202
|
+
expect.objectContaining({
|
|
203
|
+
type: 'oauth',
|
|
204
|
+
clientId: 'client-id-123',
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
expect(result.success).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('prompts for SAML config when type is saml', async () => {
|
|
211
|
+
mockPrompt
|
|
212
|
+
.mockResolvedValueOnce('saml') // type
|
|
213
|
+
.mockResolvedValueOnce('https://idp.example.com/entity') // entityId
|
|
214
|
+
.mockResolvedValueOnce('https://idp.example.com/sso') // ssoUrl
|
|
215
|
+
.mockResolvedValueOnce('-----BEGIN CERTIFICATE-----...'); // certificate
|
|
216
|
+
|
|
217
|
+
const result = await ssoCommand.execute(['add', 'enterprise-sso']);
|
|
218
|
+
|
|
219
|
+
expect(mockPrompt).toHaveBeenCalled();
|
|
220
|
+
expect(mockIdPManager.registerProvider).toHaveBeenCalledWith(
|
|
221
|
+
'enterprise-sso',
|
|
222
|
+
expect.objectContaining({
|
|
223
|
+
type: 'saml',
|
|
224
|
+
entityId: 'https://idp.example.com/entity',
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
expect(result.success).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('validates configuration before adding', async () => {
|
|
231
|
+
mockPrompt
|
|
232
|
+
.mockResolvedValueOnce('oauth')
|
|
233
|
+
.mockResolvedValueOnce('') // empty clientId - invalid
|
|
234
|
+
.mockResolvedValueOnce('secret');
|
|
235
|
+
|
|
236
|
+
const result = await ssoCommand.execute(['add', 'invalid-provider']);
|
|
237
|
+
|
|
238
|
+
expect(result.success).toBe(false);
|
|
239
|
+
expect(result.error).toContain('Client ID is required');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('uses --type flag when provided', async () => {
|
|
243
|
+
mockPrompt
|
|
244
|
+
.mockResolvedValueOnce('client-id')
|
|
245
|
+
.mockResolvedValueOnce('client-secret')
|
|
246
|
+
.mockResolvedValueOnce('https://auth.example.com')
|
|
247
|
+
.mockResolvedValueOnce('https://token.example.com')
|
|
248
|
+
.mockResolvedValueOnce('https://userinfo.example.com');
|
|
249
|
+
|
|
250
|
+
await ssoCommand.execute(['add', 'typed-provider', '--type', 'oauth']);
|
|
251
|
+
|
|
252
|
+
expect(mockIdPManager.registerProvider).toHaveBeenCalledWith(
|
|
253
|
+
'typed-provider',
|
|
254
|
+
expect.objectContaining({ type: 'oauth' })
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('execute remove', () => {
|
|
260
|
+
it('deletes provider', async () => {
|
|
261
|
+
mockPrompt.mockResolvedValueOnce('yes');
|
|
262
|
+
|
|
263
|
+
const result = await ssoCommand.execute(['remove', 'github']);
|
|
264
|
+
|
|
265
|
+
expect(mockIdPManager.removeProvider).toHaveBeenCalledWith('github');
|
|
266
|
+
expect(result.success).toBe(true);
|
|
267
|
+
expect(result.output).toContain('removed');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('confirms deletion before removing', async () => {
|
|
271
|
+
mockPrompt.mockResolvedValueOnce('no');
|
|
272
|
+
|
|
273
|
+
const result = await ssoCommand.execute(['remove', 'github']);
|
|
274
|
+
|
|
275
|
+
expect(mockIdPManager.removeProvider).not.toHaveBeenCalled();
|
|
276
|
+
expect(result.output).toContain('cancelled');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('skips confirmation with --force', async () => {
|
|
280
|
+
const result = await ssoCommand.execute(['remove', 'github', '--force']);
|
|
281
|
+
|
|
282
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
283
|
+
expect(mockIdPManager.removeProvider).toHaveBeenCalledWith('github');
|
|
284
|
+
expect(result.success).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('returns error for non-existent provider', async () => {
|
|
288
|
+
mockIdPManager.getProvider.mockReturnValue(null);
|
|
289
|
+
|
|
290
|
+
const result = await ssoCommand.execute(['remove', 'nonexistent']);
|
|
291
|
+
|
|
292
|
+
expect(result.success).toBe(false);
|
|
293
|
+
expect(result.error).toContain('not found');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('execute test', () => {
|
|
298
|
+
it('validates provider connectivity', async () => {
|
|
299
|
+
mockSSOSession.testProvider.mockResolvedValue({
|
|
300
|
+
success: true,
|
|
301
|
+
message: 'Connection successful',
|
|
302
|
+
latency: 150,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const result = await ssoCommand.execute(['test', 'github']);
|
|
306
|
+
|
|
307
|
+
expect(mockSSOSession.testProvider).toHaveBeenCalledWith('github');
|
|
308
|
+
expect(result.success).toBe(true);
|
|
309
|
+
expect(result.output).toContain('successful');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('reports errors when test fails', async () => {
|
|
313
|
+
mockSSOSession.testProvider.mockResolvedValue({
|
|
314
|
+
success: false,
|
|
315
|
+
error: 'Connection refused',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = await ssoCommand.execute(['test', 'github']);
|
|
319
|
+
|
|
320
|
+
expect(result.success).toBe(false);
|
|
321
|
+
expect(result.output).toContain('Connection refused');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('reports detailed error information', async () => {
|
|
325
|
+
mockSSOSession.testProvider.mockResolvedValue({
|
|
326
|
+
success: false,
|
|
327
|
+
error: 'Certificate expired',
|
|
328
|
+
details: {
|
|
329
|
+
expiredAt: '2025-01-01',
|
|
330
|
+
issuer: 'CN=Example CA',
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const result = await ssoCommand.execute(['test', 'okta']);
|
|
335
|
+
|
|
336
|
+
expect(result.success).toBe(false);
|
|
337
|
+
expect(result.output).toContain('Certificate expired');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('returns error for non-existent provider', async () => {
|
|
341
|
+
mockIdPManager.getProvider.mockReturnValue(null);
|
|
342
|
+
|
|
343
|
+
const result = await ssoCommand.execute(['test', 'nonexistent']);
|
|
344
|
+
|
|
345
|
+
expect(result.success).toBe(false);
|
|
346
|
+
expect(result.error).toContain('not found');
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('execute roles', () => {
|
|
351
|
+
it('shows role mappings', async () => {
|
|
352
|
+
const result = await ssoCommand.execute(['roles']);
|
|
353
|
+
|
|
354
|
+
expect(result.success).toBe(true);
|
|
355
|
+
expect(mockRoleMapper.getMappings).toHaveBeenCalled();
|
|
356
|
+
expect(result.output).toContain('admins');
|
|
357
|
+
expect(result.output).toContain('admin');
|
|
358
|
+
expect(result.output).toContain('developers');
|
|
359
|
+
expect(result.output).toContain('engineer');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('shows unmapped warning when no mappings', async () => {
|
|
363
|
+
mockRoleMapper.getMappings.mockReturnValue({
|
|
364
|
+
mappings: [],
|
|
365
|
+
defaultRole: null,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const result = await ssoCommand.execute(['roles']);
|
|
369
|
+
|
|
370
|
+
expect(result.success).toBe(true);
|
|
371
|
+
expect(result.output).toContain('No role mappings configured');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('shows default role when configured', async () => {
|
|
375
|
+
mockRoleMapper.getMappings.mockReturnValue({
|
|
376
|
+
mappings: [{ pattern: 'admins', role: 'admin', priority: 1 }],
|
|
377
|
+
defaultRole: 'engineer',
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const result = await ssoCommand.execute(['roles']);
|
|
381
|
+
|
|
382
|
+
expect(result.output).toContain('Default');
|
|
383
|
+
expect(result.output).toContain('engineer');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('shows mapping priority order', async () => {
|
|
387
|
+
const result = await ssoCommand.execute(['roles']);
|
|
388
|
+
|
|
389
|
+
// Output should show mappings in priority order
|
|
390
|
+
const output = result.output;
|
|
391
|
+
const adminsIndex = output.indexOf('admins');
|
|
392
|
+
const developersIndex = output.indexOf('developers');
|
|
393
|
+
expect(adminsIndex).toBeLessThan(developersIndex);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('execute status', () => {
|
|
398
|
+
it('shows SSO enabled status', async () => {
|
|
399
|
+
mockSSOSession.getStatus.mockReturnValue({
|
|
400
|
+
enabled: true,
|
|
401
|
+
activeSessions: 12,
|
|
402
|
+
mfaRequired: true,
|
|
403
|
+
mfaRoles: ['admin'],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const result = await ssoCommand.execute(['status']);
|
|
407
|
+
|
|
408
|
+
expect(result.success).toBe(true);
|
|
409
|
+
expect(result.output).toContain('Enabled');
|
|
410
|
+
expect(result.output).toContain('Yes');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('shows SSO disabled status', async () => {
|
|
414
|
+
mockSSOSession.getStatus.mockReturnValue({
|
|
415
|
+
enabled: false,
|
|
416
|
+
activeSessions: 0,
|
|
417
|
+
mfaRequired: false,
|
|
418
|
+
mfaRoles: [],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const result = await ssoCommand.execute(['status']);
|
|
422
|
+
|
|
423
|
+
expect(result.success).toBe(true);
|
|
424
|
+
expect(result.output).toMatch(/Enabled.*No/s);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('shows provider count', async () => {
|
|
428
|
+
const result = await ssoCommand.execute(['status']);
|
|
429
|
+
|
|
430
|
+
expect(result.output).toContain('Providers');
|
|
431
|
+
expect(result.output).toContain('3');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('shows active sessions count', async () => {
|
|
435
|
+
const result = await ssoCommand.execute(['status']);
|
|
436
|
+
|
|
437
|
+
expect(result.output).toContain('Active Sessions');
|
|
438
|
+
expect(result.output).toContain('12');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('shows MFA requirements', async () => {
|
|
442
|
+
const result = await ssoCommand.execute(['status']);
|
|
443
|
+
|
|
444
|
+
expect(result.output).toContain('MFA');
|
|
445
|
+
expect(result.output).toContain('admin');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('shows role mappings summary', async () => {
|
|
449
|
+
const result = await ssoCommand.execute(['status']);
|
|
450
|
+
|
|
451
|
+
expect(result.output).toContain('Role Mappings');
|
|
452
|
+
expect(result.output).toContain('admins');
|
|
453
|
+
expect(result.output).toContain('admin');
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
describe('formatProvider', () => {
|
|
458
|
+
it('returns readable output for OAuth provider', () => {
|
|
459
|
+
const provider = { name: 'github', type: 'oauth' };
|
|
460
|
+
const formatted = formatProvider(provider);
|
|
461
|
+
|
|
462
|
+
expect(formatted).toContain('GitHub');
|
|
463
|
+
expect(formatted).toContain('OAuth');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('returns readable output for SAML provider', () => {
|
|
467
|
+
const provider = { name: 'okta', type: 'saml' };
|
|
468
|
+
const formatted = formatProvider(provider);
|
|
469
|
+
|
|
470
|
+
expect(formatted).toContain('Okta');
|
|
471
|
+
expect(formatted).toContain('SAML');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('capitalizes provider names properly', () => {
|
|
475
|
+
const provider = { name: 'azure-ad', type: 'oauth' };
|
|
476
|
+
const formatted = formatProvider(provider);
|
|
477
|
+
|
|
478
|
+
// azure-ad should display as "Azure AD" (proper branding)
|
|
479
|
+
expect(formatted).toContain('Azure AD');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('includes connection status when provided', () => {
|
|
483
|
+
const provider = { name: 'github', type: 'oauth', connected: true };
|
|
484
|
+
const formatted = formatProvider(provider, { showStatus: true });
|
|
485
|
+
|
|
486
|
+
expect(formatted).toContain('Connected');
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('error handling', () => {
|
|
491
|
+
it('handles unknown subcommand', async () => {
|
|
492
|
+
const result = await ssoCommand.execute(['unknown']);
|
|
493
|
+
|
|
494
|
+
expect(result.success).toBe(false);
|
|
495
|
+
expect(result.error).toContain('Unknown subcommand');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('handles missing provider name for add', async () => {
|
|
499
|
+
const result = await ssoCommand.execute(['add']);
|
|
500
|
+
|
|
501
|
+
expect(result.success).toBe(false);
|
|
502
|
+
expect(result.error).toContain('Provider name required');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('handles missing provider name for remove', async () => {
|
|
506
|
+
const result = await ssoCommand.execute(['remove']);
|
|
507
|
+
|
|
508
|
+
expect(result.success).toBe(false);
|
|
509
|
+
expect(result.error).toContain('Provider name required');
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('handles missing provider name for test', async () => {
|
|
513
|
+
const result = await ssoCommand.execute(['test']);
|
|
514
|
+
|
|
515
|
+
expect(result.success).toBe(false);
|
|
516
|
+
expect(result.error).toContain('Provider name required');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('handles idpManager errors gracefully', async () => {
|
|
520
|
+
mockIdPManager.listProviders.mockImplementation(() => {
|
|
521
|
+
throw new Error('Database connection failed');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const result = await ssoCommand.execute(['providers']);
|
|
525
|
+
|
|
526
|
+
expect(result.success).toBe(false);
|
|
527
|
+
expect(result.error).toContain('Database connection failed');
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('help', () => {
|
|
532
|
+
it('shows help when no subcommand', async () => {
|
|
533
|
+
const result = await ssoCommand.execute([]);
|
|
534
|
+
|
|
535
|
+
expect(result.success).toBe(true);
|
|
536
|
+
expect(result.output).toContain('Usage');
|
|
537
|
+
expect(result.output).toContain('providers');
|
|
538
|
+
expect(result.output).toContain('add');
|
|
539
|
+
expect(result.output).toContain('remove');
|
|
540
|
+
expect(result.output).toContain('test');
|
|
541
|
+
expect(result.output).toContain('roles');
|
|
542
|
+
expect(result.output).toContain('status');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('shows help with --help flag', async () => {
|
|
546
|
+
const result = await ssoCommand.execute(['--help']);
|
|
547
|
+
|
|
548
|
+
expect(result.success).toBe(true);
|
|
549
|
+
expect(result.output).toContain('Usage');
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|