latchkey 2.21.0 → 3.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/README.md +36 -4
- package/dist/skills/generic/latchkey/SKILL.md +21 -0
- package/dist/skills/openclaw/latchkey/SKILL.md +21 -0
- package/dist/src/apiCredentials/account.d.ts +28 -0
- package/dist/src/apiCredentials/account.d.ts.map +1 -0
- package/dist/src/apiCredentials/account.js +49 -0
- package/dist/src/apiCredentials/account.js.map +1 -0
- package/dist/src/apiCredentials/store.d.ts +70 -4
- package/dist/src/apiCredentials/store.d.ts.map +1 -1
- package/dist/src/apiCredentials/store.js +161 -19
- package/dist/src/apiCredentials/store.js.map +1 -1
- package/dist/src/apiCredentials/utils.d.ts +2 -2
- package/dist/src/apiCredentials/utils.d.ts.map +1 -1
- package/dist/src/apiCredentials/utils.js +5 -5
- package/dist/src/apiCredentials/utils.js.map +1 -1
- package/dist/src/cli.js +1 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +118 -22
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +8 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/configDataStore.d.ts +3 -0
- package/dist/src/configDataStore.d.ts.map +1 -1
- package/dist/src/configDataStore.js +1 -0
- package/dist/src/configDataStore.js.map +1 -1
- package/dist/src/curl.d.ts +21 -16
- package/dist/src/curl.d.ts.map +1 -1
- package/dist/src/curl.js +17 -24
- package/dist/src/curl.js.map +1 -1
- package/dist/src/curlInjection.d.ts +8 -2
- package/dist/src/curlInjection.d.ts.map +1 -1
- package/dist/src/curlInjection.js +13 -8
- package/dist/src/curlInjection.js.map +1 -1
- package/dist/src/errorMessages.d.ts +2 -2
- package/dist/src/errorMessages.d.ts.map +1 -1
- package/dist/src/errorMessages.js +22 -6
- package/dist/src/errorMessages.js.map +1 -1
- package/dist/src/gateway/gatewayEndpoint.d.ts +1 -1
- package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -1
- package/dist/src/gateway/gatewayEndpoint.js +3 -1
- package/dist/src/gateway/gatewayEndpoint.js.map +1 -1
- package/dist/src/gateway/latchkeyEndpoint.d.ts +6 -0
- package/dist/src/gateway/latchkeyEndpoint.d.ts.map +1 -1
- package/dist/src/gateway/latchkeyEndpoint.js +6 -4
- package/dist/src/gateway/latchkeyEndpoint.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/migrations.d.ts +21 -1
- package/dist/src/migrations.d.ts.map +1 -1
- package/dist/src/migrations.js +112 -4
- package/dist/src/migrations.js.map +1 -1
- package/dist/src/oauthUtils.d.ts +2 -2
- package/dist/src/oauthUtils.d.ts.map +1 -1
- package/dist/src/oauthUtils.js +5 -5
- package/dist/src/oauthUtils.js.map +1 -1
- package/dist/src/services/aws.d.ts +1 -0
- package/dist/src/services/aws.d.ts.map +1 -1
- package/dist/src/services/aws.js +13 -0
- package/dist/src/services/aws.js.map +1 -1
- package/dist/src/services/calendly.d.ts +2 -0
- package/dist/src/services/calendly.d.ts.map +1 -1
- package/dist/src/services/calendly.js +7 -0
- package/dist/src/services/calendly.js.map +1 -1
- package/dist/src/services/coolify.d.ts +3 -1
- package/dist/src/services/coolify.d.ts.map +1 -1
- package/dist/src/services/coolify.js +10 -1
- package/dist/src/services/coolify.js.map +1 -1
- package/dist/src/services/core/base.d.ts +43 -1
- package/dist/src/services/core/base.d.ts.map +1 -1
- package/dist/src/services/core/base.js +29 -9
- package/dist/src/services/core/base.js.map +1 -1
- package/dist/src/services/core/registered.d.ts +1 -0
- package/dist/src/services/core/registered.d.ts.map +1 -1
- package/dist/src/services/core/registered.js +5 -0
- package/dist/src/services/core/registered.js.map +1 -1
- package/dist/src/services/discord.d.ts +1 -0
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +9 -0
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +2 -1
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +17 -11
- package/dist/src/services/dropbox.js.map +1 -1
- package/dist/src/services/figma.d.ts +2 -0
- package/dist/src/services/figma.d.ts.map +1 -1
- package/dist/src/services/figma.js +7 -0
- package/dist/src/services/figma.js.map +1 -1
- package/dist/src/services/github.d.ts +1 -0
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +9 -0
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/gitlab.d.ts +2 -0
- package/dist/src/services/gitlab.d.ts.map +1 -1
- package/dist/src/services/gitlab.js +9 -0
- package/dist/src/services/gitlab.js.map +1 -1
- package/dist/src/services/google/analytics.d.ts +0 -1
- package/dist/src/services/google/analytics.d.ts.map +1 -1
- package/dist/src/services/google/analytics.js +0 -3
- package/dist/src/services/google/analytics.js.map +1 -1
- package/dist/src/services/google/base.d.ts +11 -0
- package/dist/src/services/google/base.d.ts.map +1 -1
- package/dist/src/services/google/base.js +26 -7
- package/dist/src/services/google/base.js.map +1 -1
- package/dist/src/services/google/calendar.d.ts +0 -1
- package/dist/src/services/google/calendar.d.ts.map +1 -1
- package/dist/src/services/google/calendar.js +0 -3
- package/dist/src/services/google/calendar.js.map +1 -1
- package/dist/src/services/google/directions.d.ts +1 -0
- package/dist/src/services/google/directions.d.ts.map +1 -1
- package/dist/src/services/google/directions.js +5 -0
- package/dist/src/services/google/directions.js.map +1 -1
- package/dist/src/services/google/docs.d.ts +0 -1
- package/dist/src/services/google/docs.d.ts.map +1 -1
- package/dist/src/services/google/docs.js +0 -3
- package/dist/src/services/google/docs.js.map +1 -1
- package/dist/src/services/google/drive.d.ts +0 -1
- package/dist/src/services/google/drive.d.ts.map +1 -1
- package/dist/src/services/google/drive.js +0 -3
- package/dist/src/services/google/drive.js.map +1 -1
- package/dist/src/services/google/gmail.d.ts +0 -1
- package/dist/src/services/google/gmail.d.ts.map +1 -1
- package/dist/src/services/google/gmail.js +0 -3
- package/dist/src/services/google/gmail.js.map +1 -1
- package/dist/src/services/google/people.d.ts +0 -1
- package/dist/src/services/google/people.d.ts.map +1 -1
- package/dist/src/services/google/people.js +0 -3
- package/dist/src/services/google/people.js.map +1 -1
- package/dist/src/services/google/sheets.d.ts +0 -1
- package/dist/src/services/google/sheets.d.ts.map +1 -1
- package/dist/src/services/google/sheets.js +0 -3
- package/dist/src/services/google/sheets.js.map +1 -1
- package/dist/src/services/google/slides.d.ts +0 -1
- package/dist/src/services/google/slides.d.ts.map +1 -1
- package/dist/src/services/google/slides.js +0 -3
- package/dist/src/services/google/slides.js.map +1 -1
- package/dist/src/services/index.d.ts +1 -1
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/linear.d.ts +2 -1
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +8 -1
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/mailchimp.d.ts +2 -0
- package/dist/src/services/mailchimp.d.ts.map +1 -1
- package/dist/src/services/mailchimp.js +7 -0
- package/dist/src/services/mailchimp.js.map +1 -1
- package/dist/src/services/notion-mcp.d.ts +3 -2
- package/dist/src/services/notion-mcp.d.ts.map +1 -1
- package/dist/src/services/notion-mcp.js +111 -14
- package/dist/src/services/notion-mcp.js.map +1 -1
- package/dist/src/services/notion.d.ts +2 -0
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +17 -0
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/services/ramp.d.ts +11 -2
- package/dist/src/services/ramp.d.ts.map +1 -1
- package/dist/src/services/ramp.js +35 -9
- package/dist/src/services/ramp.js.map +1 -1
- package/dist/src/services/sentry.d.ts +3 -2
- package/dist/src/services/sentry.d.ts.map +1 -1
- package/dist/src/services/sentry.js +12 -27
- package/dist/src/services/sentry.js.map +1 -1
- package/dist/src/services/slack.d.ts +3 -2
- package/dist/src/services/slack.d.ts.map +1 -1
- package/dist/src/services/slack.js +19 -13
- package/dist/src/services/slack.js.map +1 -1
- package/dist/src/services/stripe.d.ts +7 -0
- package/dist/src/services/stripe.d.ts.map +1 -1
- package/dist/src/services/stripe.js +15 -0
- package/dist/src/services/stripe.js.map +1 -1
- package/dist/src/services/telegram.d.ts +1 -0
- package/dist/src/services/telegram.d.ts.map +1 -1
- package/dist/src/services/telegram.js +7 -0
- package/dist/src/services/telegram.js.map +1 -1
- package/dist/src/services/todoist.d.ts +2 -1
- package/dist/src/services/todoist.d.ts.map +1 -1
- package/dist/src/services/todoist.js +9 -1
- package/dist/src/services/todoist.js.map +1 -1
- package/dist/src/services/umami.d.ts +3 -1
- package/dist/src/services/umami.d.ts.map +1 -1
- package/dist/src/services/umami.js +11 -1
- package/dist/src/services/umami.js.map +1 -1
- package/dist/src/services/yelp.d.ts +1 -0
- package/dist/src/services/yelp.d.ts.map +1 -1
- package/dist/src/services/yelp.js +5 -0
- package/dist/src/services/yelp.js.map +1 -1
- package/dist/src/services/zoom.d.ts +8 -0
- package/dist/src/services/zoom.d.ts.map +1 -1
- package/dist/src/services/zoom.js +16 -0
- package/dist/src/services/zoom.js.map +1 -1
- package/dist/src/sharedOperations.d.ts +30 -10
- package/dist/src/sharedOperations.d.ts.map +1 -1
- package/dist/src/sharedOperations.js +67 -29
- package/dist/src/sharedOperations.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tests/apiCredentialStore.test.js +155 -2
- package/dist/tests/apiCredentialStore.test.js.map +1 -1
- package/dist/tests/apiCredentialsUtils.test.js +1 -1
- package/dist/tests/apiCredentialsUtils.test.js.map +1 -1
- package/dist/tests/cli.test.js +334 -147
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/config.test.js +5 -0
- package/dist/tests/config.test.js.map +1 -1
- package/dist/tests/gateway.test.js +8 -18
- package/dist/tests/gateway.test.js.map +1 -1
- package/dist/tests/latchkeyEndpoint.test.js +18 -28
- package/dist/tests/latchkeyEndpoint.test.js.map +1 -1
- package/dist/tests/migrations.test.js +220 -39
- package/dist/tests/migrations.test.js.map +1 -1
- package/dist/tests/mockService.d.ts +25 -0
- package/dist/tests/mockService.d.ts.map +1 -0
- package/dist/tests/mockService.js +40 -0
- package/dist/tests/mockService.js.map +1 -0
- package/dist/tests/oauthUtils.test.js +12 -12
- package/dist/tests/oauthUtils.test.js.map +1 -1
- package/dist/tests/serviceAccounts.test.d.ts +12 -0
- package/dist/tests/serviceAccounts.test.d.ts.map +1 -0
- package/dist/tests/serviceAccounts.test.js +278 -0
- package/dist/tests/serviceAccounts.test.js.map +1 -0
- package/dist/tests/sharedOperations.test.js +327 -58
- package/dist/tests/sharedOperations.test.js.map +1 -1
- package/package.json +1 -1
package/dist/tests/cli.test.js
CHANGED
|
@@ -11,8 +11,8 @@ import { EncryptedStorage } from '../src/encryptedStorage.js';
|
|
|
11
11
|
import { Config } from '../src/config.js';
|
|
12
12
|
import { ServiceRegistry } from '../src/serviceRegistry.js';
|
|
13
13
|
import { ApiCredentialStatus } from '../src/apiCredentials/base.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { Service } from '../src/services/core/base.js';
|
|
15
|
+
import { createMockService, MockService } from './mockService.js';
|
|
16
16
|
import { RegisteredService } from '../src/services/core/registered.js';
|
|
17
17
|
import { GITLAB } from '../src/services/gitlab.js';
|
|
18
18
|
import { GITHUB } from '../src/services/github.js';
|
|
@@ -33,6 +33,27 @@ function readSecureFile(path) {
|
|
|
33
33
|
const storage = new EncryptedStorage(TEST_ENCRYPTION_KEY);
|
|
34
34
|
return storage.readFile(path);
|
|
35
35
|
}
|
|
36
|
+
// Wrap a flat { service: credentials } map into the on-disk layout, storing
|
|
37
|
+
// everything as account-keyed credentials under the default account.
|
|
38
|
+
function nestAccounts(flat) {
|
|
39
|
+
return {
|
|
40
|
+
credentials: Object.fromEntries(Object.entries(flat).map(([service, creds]) => [service, { '': creds }])),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Read the raw account-keyed credentials section of the store.
|
|
44
|
+
function readStore(path) {
|
|
45
|
+
const parsed = JSON.parse(readSecureFile(path) ?? '{}');
|
|
46
|
+
return parsed.credentials ?? {};
|
|
47
|
+
}
|
|
48
|
+
// Read the raw preparations section of the store.
|
|
49
|
+
function readPreparations(path) {
|
|
50
|
+
const parsed = JSON.parse(readSecureFile(path) ?? '{}');
|
|
51
|
+
return parsed.preparations ?? {};
|
|
52
|
+
}
|
|
53
|
+
// Read a single service's default-account credentials from the store.
|
|
54
|
+
function readDefaultCredentials(path, service) {
|
|
55
|
+
return readStore(path)[service]?.[''];
|
|
56
|
+
}
|
|
36
57
|
function getCliPath() {
|
|
37
58
|
const projectRoot = join(__dirname, '..');
|
|
38
59
|
// Check both possible paths based on tsconfig setup
|
|
@@ -302,6 +323,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
302
323
|
serviceName: overrides.serviceName ?? defaultConfig.serviceName,
|
|
303
324
|
accountName: overrides.accountName ?? defaultConfig.accountName,
|
|
304
325
|
browserDisabled: overrides.browserDisabled ?? false,
|
|
326
|
+
browserEphemeral: overrides.browserEphemeral ?? false,
|
|
305
327
|
countingDisabled: overrides.countingDisabled ?? false,
|
|
306
328
|
credentialsRefreshDisabled: overrides.credentialsRefreshDisabled ?? false,
|
|
307
329
|
permissionsDoNotUseBuiltinSchemas: overrides.permissionsDoNotUseBuiltinSchemas ?? false,
|
|
@@ -318,24 +340,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
318
340
|
};
|
|
319
341
|
}
|
|
320
342
|
function createMockDependencies(overrides = {}) {
|
|
321
|
-
const mockSlackService =
|
|
322
|
-
name: 'slack',
|
|
323
|
-
displayName: 'Slack',
|
|
324
|
-
baseApiUrls: ['https://slack.com/api/'],
|
|
325
|
-
loginUrl: 'https://slack.com/signin',
|
|
326
|
-
info: 'Test info for Slack service.',
|
|
327
|
-
credentialCheckCurlArguments: ['https://slack.com/api/auth.test'],
|
|
328
|
-
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Valid),
|
|
329
|
-
setCredentialsExample(serviceName) {
|
|
330
|
-
return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
|
|
331
|
-
},
|
|
332
|
-
getCredentialsNoCurl() {
|
|
333
|
-
throw new NoCurlCredentialsNotSupportedError('slack');
|
|
334
|
-
},
|
|
335
|
-
getSession: vi.fn().mockReturnValue({
|
|
336
|
-
login: vi.fn().mockResolvedValue(new SlackApiCredentials('xoxc-test-token', 'test-cookie')),
|
|
337
|
-
}),
|
|
338
|
-
};
|
|
343
|
+
const mockSlackService = new MockService();
|
|
339
344
|
const mockRegistry = new ServiceRegistry([mockSlackService]);
|
|
340
345
|
return {
|
|
341
346
|
registry: mockRegistry,
|
|
@@ -416,9 +421,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
416
421
|
});
|
|
417
422
|
it('should include services with stored credentials when using --viable', async () => {
|
|
418
423
|
const storePath = join(tempDir, 'credentials.json');
|
|
419
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
424
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
420
425
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
421
|
-
}));
|
|
426
|
+
})));
|
|
422
427
|
const deps = createMockDependencies();
|
|
423
428
|
await runCommand(['services', 'list', '--viable'], deps);
|
|
424
429
|
expect(logs).toHaveLength(1);
|
|
@@ -451,7 +456,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
451
456
|
it('should exclude services without credentials or browser auth when using --viable', async () => {
|
|
452
457
|
const storePath = join(tempDir, 'credentials.json');
|
|
453
458
|
writeSecureFile(storePath, '{}');
|
|
454
|
-
const noLoginService = {
|
|
459
|
+
const noLoginService = createMockService({
|
|
455
460
|
name: 'nologin',
|
|
456
461
|
displayName: 'No Login Service',
|
|
457
462
|
baseApiUrls: ['https://nologin.example.com/api/'],
|
|
@@ -459,13 +464,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
459
464
|
info: 'A service without browser login support.',
|
|
460
465
|
credentialCheckCurlArguments: [],
|
|
461
466
|
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Missing),
|
|
462
|
-
setCredentialsExample(serviceName) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
getCredentialsNoCurl() {
|
|
466
|
-
throw new NoCurlCredentialsNotSupportedError('nologin');
|
|
467
|
-
},
|
|
468
|
-
};
|
|
467
|
+
setCredentialsExample: (serviceName) => `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`,
|
|
468
|
+
getSession: undefined,
|
|
469
|
+
});
|
|
469
470
|
const deps = createMockDependencies({
|
|
470
471
|
registry: new ServiceRegistry([noLoginService]),
|
|
471
472
|
});
|
|
@@ -519,9 +520,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
519
520
|
});
|
|
520
521
|
it('should include services with credentials even when no graphical environment with --viable', async () => {
|
|
521
522
|
const storePath = join(tempDir, 'credentials.json');
|
|
522
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
523
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
523
524
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
524
|
-
}));
|
|
525
|
+
})));
|
|
525
526
|
const originalPlatform = process.platform;
|
|
526
527
|
const originalDisplay = process.env.DISPLAY;
|
|
527
528
|
const originalWayland = process.env.WAYLAND_DISPLAY;
|
|
@@ -553,9 +554,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
553
554
|
});
|
|
554
555
|
it('should include services with credentials even when browser is disabled with --viable', async () => {
|
|
555
556
|
const storePath = join(tempDir, 'credentials.json');
|
|
556
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
557
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
557
558
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
558
|
-
}));
|
|
559
|
+
})));
|
|
559
560
|
const deps = createMockDependencies({
|
|
560
561
|
config: createMockConfig({ browserDisabled: true }),
|
|
561
562
|
});
|
|
@@ -566,12 +567,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
566
567
|
});
|
|
567
568
|
it('should combine --builtin and --viable filters', async () => {
|
|
568
569
|
const storePath = join(tempDir, 'credentials.json');
|
|
569
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
570
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
570
571
|
'my-gitlab': {
|
|
571
572
|
objectType: 'rawCurl',
|
|
572
573
|
curlArguments: ['-H', 'PRIVATE-TOKEN: token'],
|
|
573
574
|
},
|
|
574
|
-
}));
|
|
575
|
+
})));
|
|
575
576
|
// Ensure a graphical environment is available so browser auth is considered viable
|
|
576
577
|
const originalDisplay = process.env.DISPLAY;
|
|
577
578
|
try {
|
|
@@ -608,14 +609,14 @@ describe('CLI commands with dependency injection', () => {
|
|
|
608
609
|
expect(info.type).toBe('built-in');
|
|
609
610
|
expect(info.baseApiUrls).toEqual(['https://slack.com/api/']);
|
|
610
611
|
expect(info.authOptions).toEqual(['browser', 'set']);
|
|
611
|
-
expect(info.
|
|
612
|
+
expect(info.credentials).toEqual({});
|
|
612
613
|
expect(info.setCredentialsExample).toBe('latchkey auth set slack -H "Authorization: Bearer xoxb-your-token"');
|
|
613
614
|
expect(info.developerNotes).toBe('Test info for Slack service.');
|
|
614
615
|
});
|
|
615
616
|
it('should show auth set only for services without browser login', async () => {
|
|
616
617
|
const storePath = join(tempDir, 'credentials.json');
|
|
617
618
|
writeSecureFile(storePath, '{}');
|
|
618
|
-
const noLoginService = {
|
|
619
|
+
const noLoginService = createMockService({
|
|
619
620
|
name: 'nologin',
|
|
620
621
|
displayName: 'No Login Service',
|
|
621
622
|
baseApiUrls: ['https://nologin.example.com/api/'],
|
|
@@ -623,13 +624,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
623
624
|
info: 'A service without browser login support.',
|
|
624
625
|
credentialCheckCurlArguments: [],
|
|
625
626
|
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Missing),
|
|
626
|
-
setCredentialsExample(serviceName) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
getCredentialsNoCurl() {
|
|
630
|
-
throw new NoCurlCredentialsNotSupportedError('nologin');
|
|
631
|
-
},
|
|
632
|
-
};
|
|
627
|
+
setCredentialsExample: (serviceName) => `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`,
|
|
628
|
+
getSession: undefined,
|
|
629
|
+
});
|
|
633
630
|
const deps = createMockDependencies({
|
|
634
631
|
registry: new ServiceRegistry([noLoginService]),
|
|
635
632
|
});
|
|
@@ -649,13 +646,33 @@ describe('CLI commands with dependency injection', () => {
|
|
|
649
646
|
});
|
|
650
647
|
it('should show valid credentials status when credentials are valid', async () => {
|
|
651
648
|
const storePath = join(tempDir, 'credentials.json');
|
|
652
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
649
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
653
650
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
651
|
+
})));
|
|
652
|
+
const deps = createMockDependencies();
|
|
653
|
+
await runCommand(['services', 'info', 'slack'], deps);
|
|
654
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
655
|
+
expect(info.credentials).toEqual({
|
|
656
|
+
'': { credentialType: 'slack', credentialStatus: 'valid' },
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
it('should list credentials for each account', async () => {
|
|
660
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
661
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
662
|
+
credentials: {
|
|
663
|
+
slack: {
|
|
664
|
+
'': { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: default'] },
|
|
665
|
+
'work@example.com': { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: work'] },
|
|
666
|
+
},
|
|
667
|
+
},
|
|
654
668
|
}));
|
|
655
669
|
const deps = createMockDependencies();
|
|
656
670
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
657
671
|
const info = JSON.parse(logs[0] ?? '');
|
|
658
|
-
expect(info.
|
|
672
|
+
expect(info.credentials).toEqual({
|
|
673
|
+
'': { credentialType: 'rawCurl', credentialStatus: 'valid' },
|
|
674
|
+
'work@example.com': { credentialType: 'rawCurl', credentialStatus: 'valid' },
|
|
675
|
+
});
|
|
659
676
|
});
|
|
660
677
|
it('should return error for unknown service', async () => {
|
|
661
678
|
const deps = createMockDependencies();
|
|
@@ -677,9 +694,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
677
694
|
describe('clear command', () => {
|
|
678
695
|
it('should delete credentials for a service', async () => {
|
|
679
696
|
const storePath = join(tempDir, 'credentials.json');
|
|
680
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
697
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
681
698
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
682
|
-
}));
|
|
699
|
+
})));
|
|
683
700
|
const deps = createMockDependencies();
|
|
684
701
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
685
702
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
@@ -693,21 +710,21 @@ describe('CLI commands with dependency injection', () => {
|
|
|
693
710
|
});
|
|
694
711
|
it('should preserve other services when clearing one', async () => {
|
|
695
712
|
const storePath = join(tempDir, 'credentials.json');
|
|
696
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
713
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
697
714
|
slack: { objectType: 'slack', token: 'slack-token', dCookie: 'slack-cookie' },
|
|
698
715
|
discord: { objectType: 'authorizationBare', token: 'discord-token' },
|
|
699
|
-
}));
|
|
716
|
+
})));
|
|
700
717
|
const deps = createMockDependencies();
|
|
701
718
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
702
|
-
const storedData =
|
|
719
|
+
const storedData = readStore(storePath);
|
|
703
720
|
expect(storedData.slack).toBeUndefined();
|
|
704
721
|
expect(storedData.discord).toBeDefined();
|
|
705
|
-
expect(storedData.discord?.token).toBe('discord-token');
|
|
722
|
+
expect((storedData.discord?.['']).token).toBe('discord-token');
|
|
706
723
|
});
|
|
707
724
|
it('should delete both store and browser state with -y flag', async () => {
|
|
708
725
|
const storePath = join(tempDir, 'credentials.json');
|
|
709
726
|
const browserStatePath = join(tempDir, 'browser_state.json');
|
|
710
|
-
writeSecureFile(storePath, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
|
|
727
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } })));
|
|
711
728
|
writeSecureFile(browserStatePath, '{}');
|
|
712
729
|
const deps = createMockDependencies();
|
|
713
730
|
await runCommand(['auth', 'clear', '-y'], deps);
|
|
@@ -715,19 +732,168 @@ describe('CLI commands with dependency injection', () => {
|
|
|
715
732
|
expect(existsSync(browserStatePath)).toBe(false);
|
|
716
733
|
});
|
|
717
734
|
});
|
|
735
|
+
describe('--account option', () => {
|
|
736
|
+
const rawCurl = (token) => ({
|
|
737
|
+
objectType: 'rawCurl',
|
|
738
|
+
curlArguments: ['-H', `X-Token: ${token}`],
|
|
739
|
+
});
|
|
740
|
+
function writeMultiAccountStore(accounts) {
|
|
741
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
742
|
+
const slackAccounts = Object.fromEntries(Object.entries(accounts).map(([account, token]) => [account, rawCurl(token)]));
|
|
743
|
+
writeSecureFile(storePath, JSON.stringify({ credentials: { slack: slackAccounts } }));
|
|
744
|
+
return storePath;
|
|
745
|
+
}
|
|
746
|
+
it('auth set stores credentials under the given account', async () => {
|
|
747
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
748
|
+
writeSecureFile(storePath, '{}');
|
|
749
|
+
const deps = createMockDependencies();
|
|
750
|
+
await runCommand(['--account', 'work@example.com', 'auth', 'set', 'slack', '-H', 'X-Token: secret'], deps);
|
|
751
|
+
expect(logs).toContain('Credentials stored.');
|
|
752
|
+
const store = readStore(storePath);
|
|
753
|
+
expect(store.slack?.['work@example.com']).toEqual(rawCurl('secret'));
|
|
754
|
+
expect(store.slack?.['']).toBeUndefined();
|
|
755
|
+
});
|
|
756
|
+
it('curl injects credentials for the requested account', async () => {
|
|
757
|
+
writeMultiAccountStore({ '': 'default', 'work@example.com': 'work' });
|
|
758
|
+
const deps = createMockDependencies();
|
|
759
|
+
await runCommand(['--account', 'work@example.com', 'curl', 'https://slack.com/api/test'], deps);
|
|
760
|
+
expect(exitCode).toBe(0);
|
|
761
|
+
expect(capturedArgs).toEqual(['-H', 'X-Token: work', 'https://slack.com/api/test']);
|
|
762
|
+
});
|
|
763
|
+
it('curl uses the single stored account without requiring --account', async () => {
|
|
764
|
+
writeMultiAccountStore({ 'only@example.com': 'solo' });
|
|
765
|
+
const deps = createMockDependencies();
|
|
766
|
+
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
767
|
+
expect(exitCode).toBe(0);
|
|
768
|
+
expect(capturedArgs).toEqual(['-H', 'X-Token: solo', 'https://slack.com/api/test']);
|
|
769
|
+
});
|
|
770
|
+
it('curl requires --account when multiple accounts exist', async () => {
|
|
771
|
+
writeMultiAccountStore({ 'a@example.com': 'a', 'b@example.com': 'b' });
|
|
772
|
+
const deps = createMockDependencies();
|
|
773
|
+
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
774
|
+
expect(exitCode).toBe(1);
|
|
775
|
+
expect(errorLogs.join('\n')).toContain('--account');
|
|
776
|
+
});
|
|
777
|
+
it('curl fails gracefully for a non-existing account', async () => {
|
|
778
|
+
writeMultiAccountStore({ '': 'default' });
|
|
779
|
+
const deps = createMockDependencies();
|
|
780
|
+
await runCommand(['--account', 'missing@example.com', 'curl', 'https://slack.com/api/test'], deps);
|
|
781
|
+
// A missing account is treated as "no credentials for this service",
|
|
782
|
+
// and the suggested commands carry the requested account.
|
|
783
|
+
expect(exitCode).toBe(1);
|
|
784
|
+
expect(capturedArgs).toEqual([]);
|
|
785
|
+
expect(errorLogs.join('\n')).toContain("No credentials found for slack (account 'missing@example.com')");
|
|
786
|
+
expect(errorLogs.join('\n')).toContain('latchkey --account missing@example.com auth browser slack');
|
|
787
|
+
});
|
|
788
|
+
it('auth clear keeps the preparation without --all', async () => {
|
|
789
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
790
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
791
|
+
credentials: { slack: { 'a@example.com': rawCurl('a') } },
|
|
792
|
+
preparations: {
|
|
793
|
+
slack: { objectType: 'oauth', clientId: 'cid', clientSecret: 'csecret' },
|
|
794
|
+
},
|
|
795
|
+
}));
|
|
796
|
+
const deps = createMockDependencies();
|
|
797
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
798
|
+
expect(readStore(storePath).slack).toBeUndefined();
|
|
799
|
+
expect(readPreparations(storePath).slack).toBeDefined();
|
|
800
|
+
});
|
|
801
|
+
it('auth clear --all removes all accounts and the preparation', async () => {
|
|
802
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
803
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
804
|
+
credentials: { slack: { 'a@example.com': rawCurl('a'), 'b@example.com': rawCurl('b') } },
|
|
805
|
+
preparations: {
|
|
806
|
+
slack: { objectType: 'oauth', clientId: 'cid', clientSecret: 'csecret' },
|
|
807
|
+
},
|
|
808
|
+
}));
|
|
809
|
+
const deps = createMockDependencies();
|
|
810
|
+
await runCommand(['auth', 'clear', 'slack', '--all'], deps);
|
|
811
|
+
expect(logs.join('\n')).toContain('API credentials for slack have been cleared.');
|
|
812
|
+
expect(readStore(storePath).slack).toBeUndefined();
|
|
813
|
+
expect(readPreparations(storePath).slack).toBeUndefined();
|
|
814
|
+
});
|
|
815
|
+
it('auth clear --all rejects a missing service name', async () => {
|
|
816
|
+
const deps = createMockDependencies();
|
|
817
|
+
await runCommand(['auth', 'clear', '--all'], deps);
|
|
818
|
+
expect(exitCode).toBe(1);
|
|
819
|
+
expect(errorLogs.join('\n')).toContain('--all requires a service name');
|
|
820
|
+
});
|
|
821
|
+
it('auth clear --all rejects --account', async () => {
|
|
822
|
+
writeMultiAccountStore({ 'a@example.com': 'a' });
|
|
823
|
+
const deps = createMockDependencies();
|
|
824
|
+
await runCommand(['--account', 'a@example.com', 'auth', 'clear', 'slack', '--all'], deps);
|
|
825
|
+
expect(exitCode).toBe(1);
|
|
826
|
+
expect(errorLogs.join('\n')).toContain('--all cannot be combined with --account');
|
|
827
|
+
});
|
|
828
|
+
it('auth clear removes only the requested account', async () => {
|
|
829
|
+
const storePath = writeMultiAccountStore({
|
|
830
|
+
'a@example.com': 'a',
|
|
831
|
+
'b@example.com': 'b',
|
|
832
|
+
});
|
|
833
|
+
const deps = createMockDependencies();
|
|
834
|
+
await runCommand(['--account', 'a@example.com', 'auth', 'clear', 'slack'], deps);
|
|
835
|
+
const store = readStore(storePath);
|
|
836
|
+
expect(store.slack?.['a@example.com']).toBeUndefined();
|
|
837
|
+
expect(store.slack?.['b@example.com']).toBeDefined();
|
|
838
|
+
});
|
|
839
|
+
it('auth clear requires --account when multiple accounts exist', async () => {
|
|
840
|
+
writeMultiAccountStore({ 'a@example.com': 'a', 'b@example.com': 'b' });
|
|
841
|
+
const deps = createMockDependencies();
|
|
842
|
+
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
843
|
+
expect(exitCode).toBe(1);
|
|
844
|
+
expect(errorLogs.join('\n')).toContain('--account');
|
|
845
|
+
});
|
|
846
|
+
it('auth clear fails gracefully for a non-existing account', async () => {
|
|
847
|
+
writeMultiAccountStore({ '': 'default' });
|
|
848
|
+
const deps = createMockDependencies();
|
|
849
|
+
await runCommand(['--account', 'missing@example.com', 'auth', 'clear', 'slack'], deps);
|
|
850
|
+
// Clearing a missing account is a no-op rather than an error.
|
|
851
|
+
expect(exitCode).toBeNull();
|
|
852
|
+
expect(logs.join('\n')).toContain('No API credentials found for slack.');
|
|
853
|
+
});
|
|
854
|
+
it.each([
|
|
855
|
+
['services', 'info', 'slack'],
|
|
856
|
+
['auth', 'browser-prepare', 'slack'],
|
|
857
|
+
['auth', 'prepare', 'google-gmail', '{"clientId":"a","clientSecret":"b"}'],
|
|
858
|
+
['auth', 'list'],
|
|
859
|
+
['services', 'list'],
|
|
860
|
+
])('rejects --account for the unsupported command: %s %s', async (...commandArgs) => {
|
|
861
|
+
const deps = createMockDependencies();
|
|
862
|
+
await runCommand(['--account', 'hola@jola.com', ...commandArgs], deps);
|
|
863
|
+
expect(exitCode).toBe(1);
|
|
864
|
+
expect(errorLogs.join('\n')).toContain('--account option is not supported');
|
|
865
|
+
});
|
|
866
|
+
});
|
|
718
867
|
describe('auth list command', () => {
|
|
719
868
|
it('should list stored credentials with their status', async () => {
|
|
720
869
|
const storePath = join(tempDir, 'credentials.json');
|
|
721
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
870
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
722
871
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
723
|
-
}));
|
|
872
|
+
})));
|
|
724
873
|
const deps = createMockDependencies();
|
|
725
874
|
await runCommand(['auth', 'list'], deps);
|
|
726
875
|
expect(logs).toHaveLength(1);
|
|
727
876
|
const entries = JSON.parse(logs[0] ?? '');
|
|
728
877
|
expect(entries.slack).toEqual({
|
|
729
|
-
credentialType: 'slack',
|
|
730
|
-
|
|
878
|
+
'': { credentialType: 'slack', credentialStatus: 'valid' },
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
it('should key each service by account', async () => {
|
|
882
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
883
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
884
|
+
credentials: {
|
|
885
|
+
slack: {
|
|
886
|
+
'': { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: default'] },
|
|
887
|
+
'work@example.com': { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: work'] },
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
}));
|
|
891
|
+
const deps = createMockDependencies();
|
|
892
|
+
await runCommand(['auth', 'list'], deps);
|
|
893
|
+
const entries = JSON.parse(logs[0] ?? '');
|
|
894
|
+
expect(entries.slack).toEqual({
|
|
895
|
+
'': { credentialType: 'rawCurl', credentialStatus: 'valid' },
|
|
896
|
+
'work@example.com': { credentialType: 'rawCurl', credentialStatus: 'valid' },
|
|
731
897
|
});
|
|
732
898
|
});
|
|
733
899
|
it('should output empty object when no credentials are stored', async () => {
|
|
@@ -741,16 +907,15 @@ describe('CLI commands with dependency injection', () => {
|
|
|
741
907
|
});
|
|
742
908
|
it('should treat unknown services as valid', async () => {
|
|
743
909
|
const storePath = join(tempDir, 'credentials.json');
|
|
744
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
910
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
745
911
|
unknown: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: secret'] },
|
|
746
|
-
}));
|
|
912
|
+
})));
|
|
747
913
|
const deps = createMockDependencies();
|
|
748
914
|
await runCommand(['auth', 'list'], deps);
|
|
749
915
|
expect(logs).toHaveLength(1);
|
|
750
916
|
const entries = JSON.parse(logs[0] ?? '');
|
|
751
917
|
expect(entries.unknown).toEqual({
|
|
752
|
-
credentialType: 'rawCurl',
|
|
753
|
-
credentialStatus: 'valid',
|
|
918
|
+
'': { credentialType: 'rawCurl', credentialStatus: 'valid' },
|
|
754
919
|
});
|
|
755
920
|
});
|
|
756
921
|
});
|
|
@@ -761,8 +926,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
761
926
|
const deps = createMockDependencies();
|
|
762
927
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: secret', '-H', 'X-Other: value'], deps);
|
|
763
928
|
expect(logs).toContain('Credentials stored.');
|
|
764
|
-
|
|
765
|
-
expect(storedData.slack).toEqual({
|
|
929
|
+
expect(readDefaultCredentials(storePath, 'slack')).toEqual({
|
|
766
930
|
objectType: 'rawCurl',
|
|
767
931
|
curlArguments: ['-H', 'X-Token: secret', '-H', 'X-Other: value'],
|
|
768
932
|
});
|
|
@@ -784,14 +948,13 @@ describe('CLI commands with dependency injection', () => {
|
|
|
784
948
|
});
|
|
785
949
|
it('should overwrite existing credentials', async () => {
|
|
786
950
|
const storePath = join(tempDir, 'credentials.json');
|
|
787
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
951
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
788
952
|
slack: { objectType: 'slack', token: 'old-token', dCookie: 'old-cookie' },
|
|
789
|
-
}));
|
|
953
|
+
})));
|
|
790
954
|
const deps = createMockDependencies();
|
|
791
955
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: new-secret'], deps);
|
|
792
956
|
expect(logs).toContain('Credentials stored.');
|
|
793
|
-
|
|
794
|
-
expect(storedData.slack).toEqual({
|
|
957
|
+
expect(readDefaultCredentials(storePath, 'slack')).toEqual({
|
|
795
958
|
objectType: 'rawCurl',
|
|
796
959
|
curlArguments: ['-H', 'X-Token: new-secret'],
|
|
797
960
|
});
|
|
@@ -802,7 +965,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
802
965
|
const NEW_ENCRYPTION_KEY = 'Cc3mOUF63KFLOU3oJwFdfCBaMWlvjlbgxkcLlX+bEzM=';
|
|
803
966
|
function readWithKey(path, key) {
|
|
804
967
|
const storage = new EncryptedStorage(key);
|
|
805
|
-
|
|
968
|
+
const raw = JSON.parse(storage.readFile(path) ?? '{}');
|
|
969
|
+
// Unwrap the default account so assertions can compare flat credentials.
|
|
970
|
+
return Object.fromEntries(Object.entries(raw.credentials ?? {}).map(([service, accounts]) => [service, accounts['']]));
|
|
806
971
|
}
|
|
807
972
|
function withStdinKey(key, overrides = {}) {
|
|
808
973
|
return createMockDependencies({ readStdin: () => Promise.resolve(key), ...overrides });
|
|
@@ -810,7 +975,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
810
975
|
// The credential store filename used by the mock config.
|
|
811
976
|
const STORE_FILENAME = 'credentials.json';
|
|
812
977
|
function writeStore(contents) {
|
|
813
|
-
writeSecureFile(join(tempDir, STORE_FILENAME), JSON.stringify(contents));
|
|
978
|
+
writeSecureFile(join(tempDir, STORE_FILENAME), JSON.stringify(nestAccounts(contents)));
|
|
814
979
|
}
|
|
815
980
|
it('re-encrypts all stored credentials with the new key', async () => {
|
|
816
981
|
writeStore({
|
|
@@ -920,8 +1085,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
920
1085
|
});
|
|
921
1086
|
await runCommand(['auth', 'set-nocurl', 'telegram', '123456:ABC-DEF'], deps);
|
|
922
1087
|
expect(logs).toContain('Credentials stored.');
|
|
923
|
-
|
|
924
|
-
expect(storedData.telegram).toEqual({
|
|
1088
|
+
expect(readDefaultCredentials(storePath, 'telegram')).toEqual({
|
|
925
1089
|
objectType: 'telegramBot',
|
|
926
1090
|
token: '123456:ABC-DEF',
|
|
927
1091
|
});
|
|
@@ -952,7 +1116,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
952
1116
|
});
|
|
953
1117
|
});
|
|
954
1118
|
describe('prepare command', () => {
|
|
955
|
-
it(
|
|
1119
|
+
it("stores an OAuth client as the service's preparation and reports success", async () => {
|
|
956
1120
|
const storePath = join(tempDir, 'credentials.json');
|
|
957
1121
|
writeSecureFile(storePath, '{}');
|
|
958
1122
|
const deps = createMockDependencies({
|
|
@@ -960,12 +1124,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
960
1124
|
});
|
|
961
1125
|
await runCommand(['auth', 'prepare', 'google-gmail', '{"clientId":"cid","clientSecret":"csecret"}'], deps);
|
|
962
1126
|
expect(logs).toContain('Done');
|
|
963
|
-
|
|
964
|
-
expect(storedData['google-gmail']).toEqual({
|
|
1127
|
+
expect(readPreparations(storePath)['google-gmail']).toEqual({
|
|
965
1128
|
objectType: 'oauth',
|
|
966
1129
|
clientId: 'cid',
|
|
967
1130
|
clientSecret: 'csecret',
|
|
968
1131
|
});
|
|
1132
|
+
expect(readDefaultCredentials(storePath, 'google-gmail')).toBeUndefined();
|
|
969
1133
|
});
|
|
970
1134
|
it('returns an error for an unknown service', async () => {
|
|
971
1135
|
const deps = createMockDependencies();
|
|
@@ -1003,9 +1167,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1003
1167
|
describe('curl command', () => {
|
|
1004
1168
|
it('should pass arguments to subprocess', async () => {
|
|
1005
1169
|
const storePath = join(tempDir, 'credentials.json');
|
|
1006
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1170
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1007
1171
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1008
|
-
}));
|
|
1172
|
+
})));
|
|
1009
1173
|
const deps = createMockDependencies();
|
|
1010
1174
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
1011
1175
|
expect(capturedArgs).toEqual([
|
|
@@ -1019,9 +1183,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1019
1183
|
});
|
|
1020
1184
|
it('should pass raw curl credentials to subprocess', async () => {
|
|
1021
1185
|
const storePath = join(tempDir, 'credentials.json');
|
|
1022
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1186
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1023
1187
|
slack: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Custom: header'] },
|
|
1024
|
-
}));
|
|
1188
|
+
})));
|
|
1025
1189
|
const deps = createMockDependencies();
|
|
1026
1190
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
1027
1191
|
expect(capturedArgs).toEqual(['-H', 'X-Custom: header', 'https://slack.com/api/test']);
|
|
@@ -1029,9 +1193,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1029
1193
|
});
|
|
1030
1194
|
it('should pass multiple arguments correctly', async () => {
|
|
1031
1195
|
const storePath = join(tempDir, 'credentials.json');
|
|
1032
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1196
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1033
1197
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1034
|
-
}));
|
|
1198
|
+
})));
|
|
1035
1199
|
const deps = createMockDependencies();
|
|
1036
1200
|
await runCommand([
|
|
1037
1201
|
'curl',
|
|
@@ -1051,9 +1215,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1051
1215
|
});
|
|
1052
1216
|
it('should return subprocess exit code', async () => {
|
|
1053
1217
|
const storePath = join(tempDir, 'credentials.json');
|
|
1054
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1218
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1055
1219
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1056
|
-
}));
|
|
1220
|
+
})));
|
|
1057
1221
|
const deps = createMockDependencies({
|
|
1058
1222
|
runCurl: () => ({ returncode: 42, stdout: '', stderr: '' }),
|
|
1059
1223
|
});
|
|
@@ -1072,9 +1236,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1072
1236
|
});
|
|
1073
1237
|
it('should return error when OAuth credentials lack an access token', async () => {
|
|
1074
1238
|
const storePath = join(tempDir, 'credentials.json');
|
|
1075
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1239
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1076
1240
|
'google-gmail': { objectType: 'oauth', clientId: 'cid', clientSecret: 'csecret' },
|
|
1077
|
-
}));
|
|
1241
|
+
})));
|
|
1078
1242
|
const deps = createMockDependencies({
|
|
1079
1243
|
registry: new ServiceRegistry([GOOGLE_GMAIL]),
|
|
1080
1244
|
});
|
|
@@ -1084,14 +1248,14 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1084
1248
|
});
|
|
1085
1249
|
it('should route a shared Drive files call to the Docs service when only Docs is authed', async () => {
|
|
1086
1250
|
const storePath = join(tempDir, 'credentials.json');
|
|
1087
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1251
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1088
1252
|
'google-docs': {
|
|
1089
1253
|
objectType: 'oauth',
|
|
1090
1254
|
clientId: 'cid',
|
|
1091
1255
|
clientSecret: 'csecret',
|
|
1092
1256
|
accessToken: 'docs-token',
|
|
1093
1257
|
},
|
|
1094
|
-
}));
|
|
1258
|
+
})));
|
|
1095
1259
|
const deps = createMockDependencies({
|
|
1096
1260
|
registry: new ServiceRegistry([GOOGLE_DRIVE, GOOGLE_DOCS]),
|
|
1097
1261
|
});
|
|
@@ -1101,7 +1265,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1101
1265
|
});
|
|
1102
1266
|
it('should prefer the canonical Drive service for a shared Drive files call when both are authed', async () => {
|
|
1103
1267
|
const storePath = join(tempDir, 'credentials.json');
|
|
1104
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1268
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1105
1269
|
'google-drive': {
|
|
1106
1270
|
objectType: 'oauth',
|
|
1107
1271
|
clientId: 'cid',
|
|
@@ -1114,7 +1278,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1114
1278
|
clientSecret: 'csecret',
|
|
1115
1279
|
accessToken: 'docs-token',
|
|
1116
1280
|
},
|
|
1117
|
-
}));
|
|
1281
|
+
})));
|
|
1118
1282
|
const deps = createMockDependencies({
|
|
1119
1283
|
registry: new ServiceRegistry([GOOGLE_DRIVE, GOOGLE_DOCS]),
|
|
1120
1284
|
});
|
|
@@ -1124,7 +1288,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1124
1288
|
});
|
|
1125
1289
|
it('should skip an expired candidate in favor of one with unexpired credentials', async () => {
|
|
1126
1290
|
const storePath = join(tempDir, 'credentials.json');
|
|
1127
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1291
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1128
1292
|
'google-drive': {
|
|
1129
1293
|
objectType: 'oauth',
|
|
1130
1294
|
clientId: 'cid',
|
|
@@ -1138,7 +1302,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1138
1302
|
clientSecret: 'csecret',
|
|
1139
1303
|
accessToken: 'docs-token',
|
|
1140
1304
|
},
|
|
1141
|
-
}));
|
|
1305
|
+
})));
|
|
1142
1306
|
const deps = createMockDependencies({
|
|
1143
1307
|
registry: new ServiceRegistry([GOOGLE_DRIVE, GOOGLE_DOCS]),
|
|
1144
1308
|
config: createMockConfig({ credentialsRefreshDisabled: true }),
|
|
@@ -1149,14 +1313,14 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1149
1313
|
});
|
|
1150
1314
|
it('should not route a non-files Drive call to the Docs service', async () => {
|
|
1151
1315
|
const storePath = join(tempDir, 'credentials.json');
|
|
1152
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1316
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1153
1317
|
'google-docs': {
|
|
1154
1318
|
objectType: 'oauth',
|
|
1155
1319
|
clientId: 'cid',
|
|
1156
1320
|
clientSecret: 'csecret',
|
|
1157
1321
|
accessToken: 'docs-token',
|
|
1158
1322
|
},
|
|
1159
|
-
}));
|
|
1323
|
+
})));
|
|
1160
1324
|
const deps = createMockDependencies({
|
|
1161
1325
|
registry: new ServiceRegistry([GOOGLE_DRIVE, GOOGLE_DOCS]),
|
|
1162
1326
|
});
|
|
@@ -1186,9 +1350,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1186
1350
|
});
|
|
1187
1351
|
it('should still inject credentials for known services when passthroughUnknown is enabled', async () => {
|
|
1188
1352
|
const storePath = join(tempDir, 'credentials.json');
|
|
1189
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1353
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1190
1354
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1191
|
-
}));
|
|
1355
|
+
})));
|
|
1192
1356
|
const deps = createMockDependencies({
|
|
1193
1357
|
config: createMockConfig({ passthroughUnknown: true }),
|
|
1194
1358
|
});
|
|
@@ -1198,26 +1362,15 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1198
1362
|
});
|
|
1199
1363
|
it('should read credentials from store and not call login', async () => {
|
|
1200
1364
|
const storePath = join(tempDir, 'credentials.json');
|
|
1201
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1365
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1202
1366
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1203
|
-
}));
|
|
1367
|
+
})));
|
|
1204
1368
|
const mockLogin = vi.fn();
|
|
1205
|
-
const mockSlackService = {
|
|
1206
|
-
name: 'slack',
|
|
1207
|
-
displayName: 'Slack',
|
|
1208
|
-
baseApiUrls: ['https://slack.com/api/'],
|
|
1209
|
-
loginUrl: 'https://slack.com/signin',
|
|
1210
|
-
info: 'Test info for Slack service.',
|
|
1369
|
+
const mockSlackService = createMockService({
|
|
1211
1370
|
credentialCheckCurlArguments: [],
|
|
1212
1371
|
checkApiCredentials: vi.fn(),
|
|
1213
|
-
setCredentialsExample(serviceName) {
|
|
1214
|
-
return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
|
|
1215
|
-
},
|
|
1216
|
-
getCredentialsNoCurl() {
|
|
1217
|
-
throw new NoCurlCredentialsNotSupportedError('slack');
|
|
1218
|
-
},
|
|
1219
1372
|
getSession: vi.fn().mockReturnValue({ login: mockLogin }),
|
|
1220
|
-
};
|
|
1373
|
+
});
|
|
1221
1374
|
const deps = createMockDependencies({
|
|
1222
1375
|
registry: new ServiceRegistry([mockSlackService]),
|
|
1223
1376
|
});
|
|
@@ -1234,9 +1387,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1234
1387
|
});
|
|
1235
1388
|
it('should inject telegram bot token into URL path', async () => {
|
|
1236
1389
|
const storePath = join(tempDir, 'credentials.json');
|
|
1237
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1390
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1238
1391
|
telegram: { objectType: 'telegramBot', token: '123456:ABC-DEF' },
|
|
1239
|
-
}));
|
|
1392
|
+
})));
|
|
1240
1393
|
const deps = createMockDependencies({
|
|
1241
1394
|
registry: new ServiceRegistry([TELEGRAM]),
|
|
1242
1395
|
});
|
|
@@ -1246,10 +1399,10 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1246
1399
|
});
|
|
1247
1400
|
it('should work when service does not have getSession but credentials exist', async () => {
|
|
1248
1401
|
const storePath = join(tempDir, 'credentials.json');
|
|
1249
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1402
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1250
1403
|
nologin: { objectType: 'rawCurl', curlArguments: ['-H', 'X-API-Key: secret'] },
|
|
1251
|
-
}));
|
|
1252
|
-
const noLoginService = {
|
|
1404
|
+
})));
|
|
1405
|
+
const noLoginService = createMockService({
|
|
1253
1406
|
name: 'nologin',
|
|
1254
1407
|
displayName: 'No Login Service',
|
|
1255
1408
|
baseApiUrls: ['https://nologin.example.com/api/'],
|
|
@@ -1257,14 +1410,10 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1257
1410
|
info: 'A service without browser login support.',
|
|
1258
1411
|
credentialCheckCurlArguments: [],
|
|
1259
1412
|
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Valid),
|
|
1260
|
-
setCredentialsExample(serviceName) {
|
|
1261
|
-
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
1262
|
-
},
|
|
1263
|
-
getCredentialsNoCurl() {
|
|
1264
|
-
throw new NoCurlCredentialsNotSupportedError('nologin');
|
|
1265
|
-
},
|
|
1413
|
+
setCredentialsExample: (serviceName) => `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`,
|
|
1266
1414
|
// No getSession - service doesn't support browser login
|
|
1267
|
-
|
|
1415
|
+
getSession: undefined,
|
|
1416
|
+
});
|
|
1268
1417
|
const deps = createMockDependencies({
|
|
1269
1418
|
registry: new ServiceRegistry([noLoginService]),
|
|
1270
1419
|
});
|
|
@@ -1275,9 +1424,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1275
1424
|
});
|
|
1276
1425
|
it('should reject request when permission check denies it', async () => {
|
|
1277
1426
|
const storePath = join(tempDir, 'credentials.json');
|
|
1278
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1427
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1279
1428
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1280
|
-
}));
|
|
1429
|
+
})));
|
|
1281
1430
|
const deps = createMockDependencies({
|
|
1282
1431
|
checkPermission: () => Promise.resolve(false),
|
|
1283
1432
|
});
|
|
@@ -1288,9 +1437,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1288
1437
|
});
|
|
1289
1438
|
it('should allow request when permission check approves it', async () => {
|
|
1290
1439
|
const storePath = join(tempDir, 'credentials.json');
|
|
1291
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1440
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1292
1441
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1293
|
-
}));
|
|
1442
|
+
})));
|
|
1294
1443
|
const deps = createMockDependencies({
|
|
1295
1444
|
checkPermission: () => Promise.resolve(true),
|
|
1296
1445
|
});
|
|
@@ -1300,9 +1449,9 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1300
1449
|
});
|
|
1301
1450
|
it('should exit with error when permission check fails', async () => {
|
|
1302
1451
|
const storePath = join(tempDir, 'credentials.json');
|
|
1303
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1452
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1304
1453
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
1305
|
-
}));
|
|
1454
|
+
})));
|
|
1306
1455
|
const { PermissionCheckError } = await import('../src/permissions.js');
|
|
1307
1456
|
const deps = createMockDependencies({
|
|
1308
1457
|
checkPermission: () => {
|
|
@@ -1317,7 +1466,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1317
1466
|
});
|
|
1318
1467
|
describe('auth browser command', () => {
|
|
1319
1468
|
it('should return error when service does not support browser login', async () => {
|
|
1320
|
-
const noLoginService = {
|
|
1469
|
+
const noLoginService = createMockService({
|
|
1321
1470
|
name: 'nologin',
|
|
1322
1471
|
displayName: 'No Login Service',
|
|
1323
1472
|
baseApiUrls: ['https://nologin.example.com/api/'],
|
|
@@ -1325,13 +1474,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1325
1474
|
info: 'A service without browser login support.',
|
|
1326
1475
|
credentialCheckCurlArguments: [],
|
|
1327
1476
|
checkApiCredentials: vi.fn(),
|
|
1328
|
-
setCredentialsExample(serviceName) {
|
|
1329
|
-
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
1330
|
-
},
|
|
1477
|
+
setCredentialsExample: (serviceName) => `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`,
|
|
1331
1478
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
1332
1479
|
getCredentialsNoCurl: Service.prototype.getCredentialsNoCurl,
|
|
1333
1480
|
// No getSession - service doesn't support browser login
|
|
1334
|
-
|
|
1481
|
+
getSession: undefined,
|
|
1482
|
+
});
|
|
1335
1483
|
const deps = createMockDependencies({
|
|
1336
1484
|
registry: new ServiceRegistry([noLoginService]),
|
|
1337
1485
|
});
|
|
@@ -1370,7 +1518,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1370
1518
|
}
|
|
1371
1519
|
});
|
|
1372
1520
|
it('should suggest set-nocurl when service supports nocurl credentials', async () => {
|
|
1373
|
-
const nocurlService = {
|
|
1521
|
+
const nocurlService = createMockService({
|
|
1374
1522
|
name: 'nocurl-only',
|
|
1375
1523
|
displayName: 'NoCurl Only Service',
|
|
1376
1524
|
baseApiUrls: ['https://nocurl.example.com/api/'],
|
|
@@ -1378,9 +1526,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1378
1526
|
info: 'A service with nocurl credentials but no browser login.',
|
|
1379
1527
|
credentialCheckCurlArguments: [],
|
|
1380
1528
|
checkApiCredentials: vi.fn(),
|
|
1381
|
-
setCredentialsExample(serviceName) {
|
|
1382
|
-
return `latchkey auth set-nocurl ${serviceName} <some-arg>`;
|
|
1383
|
-
},
|
|
1529
|
+
setCredentialsExample: (serviceName) => `latchkey auth set-nocurl ${serviceName} <some-arg>`,
|
|
1384
1530
|
getCredentialsNoCurl(arguments_) {
|
|
1385
1531
|
if (arguments_.length !== 1) {
|
|
1386
1532
|
throw new Error('Expected exactly one argument');
|
|
@@ -1388,7 +1534,8 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1388
1534
|
return { objectType: 'test', injectIntoCurlCall: vi.fn(), isExpired: () => false };
|
|
1389
1535
|
},
|
|
1390
1536
|
// No getSession - service doesn't support browser login
|
|
1391
|
-
|
|
1537
|
+
getSession: undefined,
|
|
1538
|
+
});
|
|
1392
1539
|
const deps = createMockDependencies({
|
|
1393
1540
|
registry: new ServiceRegistry([nocurlService]),
|
|
1394
1541
|
});
|
|
@@ -1659,12 +1806,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1659
1806
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1660
1807
|
// Store credentials
|
|
1661
1808
|
const storePath = join(tempDir, 'credentials.json');
|
|
1662
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1809
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1663
1810
|
'my-api': {
|
|
1664
1811
|
objectType: 'rawCurl',
|
|
1665
1812
|
curlArguments: ['-H', 'Authorization: Bearer my-token'],
|
|
1666
1813
|
},
|
|
1667
|
-
}));
|
|
1814
|
+
})));
|
|
1668
1815
|
logs = [];
|
|
1669
1816
|
exitCode = null;
|
|
1670
1817
|
capturedArgs = [];
|
|
@@ -1713,12 +1860,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1713
1860
|
], deps);
|
|
1714
1861
|
// Store credentials
|
|
1715
1862
|
const storePath = join(tempDir, 'credentials.json');
|
|
1716
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1863
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1717
1864
|
'my-gitlab': {
|
|
1718
1865
|
objectType: 'rawCurl',
|
|
1719
1866
|
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
1720
1867
|
},
|
|
1721
|
-
}));
|
|
1868
|
+
})));
|
|
1722
1869
|
logs = [];
|
|
1723
1870
|
exitCode = null;
|
|
1724
1871
|
capturedArgs = [];
|
|
@@ -1796,19 +1943,51 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1796
1943
|
], deps);
|
|
1797
1944
|
// Store credentials for it
|
|
1798
1945
|
const storePath = join(tempDir, 'credentials.json');
|
|
1799
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
1946
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1800
1947
|
'my-gitlab': {
|
|
1801
1948
|
objectType: 'rawCurl',
|
|
1802
1949
|
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
1803
1950
|
},
|
|
1951
|
+
})));
|
|
1952
|
+
logs = [];
|
|
1953
|
+
errorLogs = [];
|
|
1954
|
+
exitCode = null;
|
|
1955
|
+
await runCommand(['services', 'deregister', 'my-gitlab'], deps);
|
|
1956
|
+
expect(exitCode).toBe(1);
|
|
1957
|
+
expect(errorLogs[0]).toContain('Credentials or a preparation still exist');
|
|
1958
|
+
expect(errorLogs[0]).toContain('latchkey auth clear my-gitlab --all');
|
|
1959
|
+
// Service should still be in config
|
|
1960
|
+
const entries = loadRegisteredServices(deps.config.configPath);
|
|
1961
|
+
expect(entries.get('my-gitlab')).toBeDefined();
|
|
1962
|
+
});
|
|
1963
|
+
it('should reject deregistering when only a preparation still exists', async () => {
|
|
1964
|
+
const deps = createMockDependencies({
|
|
1965
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1966
|
+
});
|
|
1967
|
+
await runCommand([
|
|
1968
|
+
'services',
|
|
1969
|
+
'register',
|
|
1970
|
+
'my-gitlab',
|
|
1971
|
+
'--base-api-url',
|
|
1972
|
+
'https://gitlab.mycompany.com/api/',
|
|
1973
|
+
'--service-family',
|
|
1974
|
+
'gitlab',
|
|
1975
|
+
], deps);
|
|
1976
|
+
// Store a preparation (but no credentials) for the service
|
|
1977
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
1978
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1979
|
+
credentials: {},
|
|
1980
|
+
preparations: {
|
|
1981
|
+
'my-gitlab': { objectType: 'oauth', clientId: 'cid', clientSecret: 'csecret' },
|
|
1982
|
+
},
|
|
1804
1983
|
}));
|
|
1805
1984
|
logs = [];
|
|
1806
1985
|
errorLogs = [];
|
|
1807
1986
|
exitCode = null;
|
|
1808
1987
|
await runCommand(['services', 'deregister', 'my-gitlab'], deps);
|
|
1809
1988
|
expect(exitCode).toBe(1);
|
|
1810
|
-
expect(errorLogs[0]).toContain('Credentials still exist');
|
|
1811
|
-
expect(errorLogs[0]).toContain('latchkey auth clear my-gitlab');
|
|
1989
|
+
expect(errorLogs[0]).toContain('Credentials or a preparation still exist');
|
|
1990
|
+
expect(errorLogs[0]).toContain('latchkey auth clear my-gitlab --all');
|
|
1812
1991
|
// Service should still be in config
|
|
1813
1992
|
const entries = loadRegisteredServices(deps.config.configPath);
|
|
1814
1993
|
expect(entries.get('my-gitlab')).toBeDefined();
|
|
@@ -1829,12 +2008,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1829
2008
|
], deps);
|
|
1830
2009
|
// Store and then clear credentials
|
|
1831
2010
|
const storePath = join(tempDir, 'credentials.json');
|
|
1832
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
2011
|
+
writeSecureFile(storePath, JSON.stringify(nestAccounts({
|
|
1833
2012
|
'my-gitlab': {
|
|
1834
2013
|
objectType: 'rawCurl',
|
|
1835
2014
|
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
1836
2015
|
},
|
|
1837
|
-
}));
|
|
2016
|
+
})));
|
|
1838
2017
|
logs = [];
|
|
1839
2018
|
errorLogs = [];
|
|
1840
2019
|
exitCode = null;
|
|
@@ -1998,6 +2177,14 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1998
2177
|
});
|
|
1999
2178
|
expect(logs).toContain('Done');
|
|
2000
2179
|
});
|
|
2180
|
+
it('reports the account returned by the gateway for `auth browser`', async () => {
|
|
2181
|
+
makeFetchMock(new Response(JSON.stringify({ result: { account: 'user@example.com' } }), { status: 200 }));
|
|
2182
|
+
const deps = createMockDependencies({
|
|
2183
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
2184
|
+
});
|
|
2185
|
+
await runCommand(['auth', 'browser', 'slack'], deps);
|
|
2186
|
+
expect(logs).toContain("Done. Stored credentials for account 'user@example.com'.");
|
|
2187
|
+
});
|
|
2001
2188
|
it('forwards `auth browser-prepare` and reports `Already prepared.` when the gateway says so', async () => {
|
|
2002
2189
|
makeFetchMock(new Response(JSON.stringify({ result: { alreadyPrepared: true } }), { status: 200 }));
|
|
2003
2190
|
const deps = createMockDependencies({
|