latchkey 2.6.0 → 2.7.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 +63 -7
- package/dist/scripts/recordBrowserSession.js +3 -3
- package/dist/scripts/recordBrowserSession.js.map +1 -1
- package/dist/src/{apiCredentials.d.ts → apiCredentials/base.d.ts} +6 -6
- package/dist/src/apiCredentials/base.d.ts.map +1 -0
- package/dist/src/{apiCredentials.js → apiCredentials/base.js} +5 -5
- package/dist/src/apiCredentials/base.js.map +1 -0
- package/dist/src/{apiCredentialsSerialization.d.ts → apiCredentials/serialization.d.ts} +5 -5
- package/dist/src/apiCredentials/serialization.d.ts.map +1 -0
- package/dist/src/{apiCredentialsSerialization.js → apiCredentials/serialization.js} +9 -9
- package/dist/src/apiCredentials/serialization.js.map +1 -0
- package/dist/src/{apiCredentialStore.d.ts → apiCredentials/store.d.ts} +3 -3
- package/dist/src/apiCredentials/store.d.ts.map +1 -0
- package/dist/src/{apiCredentialStore.js → apiCredentials/store.js} +2 -2
- package/dist/src/apiCredentials/store.js.map +1 -0
- package/dist/src/apiCredentials/utils.d.ts +13 -0
- package/dist/src/apiCredentials/utils.d.ts.map +1 -0
- package/dist/src/apiCredentials/utils.js +27 -0
- package/dist/src/apiCredentials/utils.js.map +1 -0
- package/dist/src/cli.js +42 -39
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts +6 -3
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +243 -190
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +36 -2
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +112 -17
- package/dist/src/config.js.map +1 -1
- package/dist/src/configDataStore.d.ts +44 -0
- package/dist/src/configDataStore.d.ts.map +1 -1
- package/dist/src/configDataStore.js +27 -0
- package/dist/src/configDataStore.js.map +1 -1
- package/dist/src/curl.d.ts +41 -8
- package/dist/src/curl.d.ts.map +1 -1
- package/dist/src/curl.js +80 -75
- package/dist/src/curl.js.map +1 -1
- package/dist/src/curlInjection.d.ts +46 -0
- package/dist/src/curlInjection.d.ts.map +1 -0
- package/dist/src/curlInjection.js +99 -0
- package/dist/src/curlInjection.js.map +1 -0
- package/dist/src/errorMessages.d.ts +14 -0
- package/dist/src/errorMessages.d.ts.map +1 -0
- package/dist/src/errorMessages.js +22 -0
- package/dist/src/errorMessages.js.map +1 -0
- package/dist/src/gateway/client.d.ts +32 -0
- package/dist/src/gateway/client.d.ts.map +1 -0
- package/dist/src/gateway/client.js +89 -0
- package/dist/src/gateway/client.js.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts +43 -0
- package/dist/src/gateway/gatewayEndpoint.d.ts.map +1 -0
- package/dist/src/gateway/gatewayEndpoint.js +297 -0
- package/dist/src/gateway/gatewayEndpoint.js.map +1 -0
- package/dist/src/gateway/latchkeyEndpoint.d.ts +105 -0
- package/dist/src/gateway/latchkeyEndpoint.d.ts.map +1 -0
- package/dist/src/gateway/latchkeyEndpoint.js +144 -0
- package/dist/src/gateway/latchkeyEndpoint.js.map +1 -0
- package/dist/src/gateway/server.d.ts +20 -0
- package/dist/src/gateway/server.d.ts.map +1 -0
- package/dist/src/gateway/server.js +90 -0
- package/dist/src/gateway/server.js.map +1 -0
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/permissions.d.ts +2 -1
- package/dist/src/permissions.d.ts.map +1 -1
- package/dist/src/permissions.js +8 -4
- package/dist/src/permissions.js.map +1 -1
- package/dist/src/{registry.d.ts → serviceRegistry.d.ts} +4 -4
- package/dist/src/serviceRegistry.d.ts.map +1 -0
- package/dist/src/{registry.js → serviceRegistry.js} +4 -4
- package/dist/src/serviceRegistry.js.map +1 -0
- package/dist/src/services/aws.d.ts +2 -2
- package/dist/src/services/aws.d.ts.map +1 -1
- package/dist/src/services/aws.js +17 -10
- package/dist/src/services/aws.js.map +1 -1
- package/dist/src/services/core/base.d.ts +2 -2
- package/dist/src/services/core/base.d.ts.map +1 -1
- package/dist/src/services/core/base.js +3 -3
- package/dist/src/services/core/base.js.map +1 -1
- package/dist/src/services/core/registered.d.ts +2 -2
- package/dist/src/services/core/registered.d.ts.map +1 -1
- package/dist/src/services/core/registered.js +2 -2
- package/dist/src/services/core/registered.js.map +1 -1
- package/dist/src/services/discord.d.ts +1 -1
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +1 -1
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +1 -1
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +1 -1
- package/dist/src/services/dropbox.js.map +1 -1
- package/dist/src/services/github.d.ts +1 -1
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +1 -1
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/google/base.d.ts +2 -2
- package/dist/src/services/google/base.d.ts.map +1 -1
- package/dist/src/services/google/base.js +3 -3
- package/dist/src/services/google/base.js.map +1 -1
- package/dist/src/services/google/directions.d.ts +1 -1
- package/dist/src/services/google/directions.d.ts.map +1 -1
- package/dist/src/services/linear.d.ts +1 -1
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +1 -1
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/notion.d.ts +1 -1
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +1 -1
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/services/sentry.d.ts +2 -2
- package/dist/src/services/sentry.d.ts.map +1 -1
- package/dist/src/services/sentry.js +6 -3
- package/dist/src/services/sentry.js.map +1 -1
- package/dist/src/services/slack.d.ts +3 -3
- package/dist/src/services/slack.d.ts.map +1 -1
- package/dist/src/services/slack.js +5 -5
- package/dist/src/services/slack.js.map +1 -1
- package/dist/src/services/telegram.d.ts +2 -2
- package/dist/src/services/telegram.d.ts.map +1 -1
- package/dist/src/services/telegram.js +2 -2
- package/dist/src/services/telegram.js.map +1 -1
- package/dist/src/sharedOperations.d.ts +44 -0
- package/dist/src/sharedOperations.d.ts.map +1 -0
- package/dist/src/sharedOperations.js +131 -0
- package/dist/src/sharedOperations.js.map +1 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +4 -0
- package/dist/src/version.js.map +1 -0
- package/dist/tests/apiCredentialStore.test.js +2 -2
- package/dist/tests/apiCredentialStore.test.js.map +1 -1
- package/dist/tests/apiCredentials.test.js +37 -36
- package/dist/tests/apiCredentials.test.js.map +1 -1
- package/dist/tests/cli.test.js +241 -55
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/config.test.d.ts +2 -0
- package/dist/tests/config.test.d.ts.map +1 -0
- package/dist/tests/config.test.js +150 -0
- package/dist/tests/config.test.js.map +1 -0
- package/dist/tests/gateway.test.d.ts +2 -0
- package/dist/tests/gateway.test.d.ts.map +1 -0
- package/dist/tests/gateway.test.js +566 -0
- package/dist/tests/gateway.test.js.map +1 -0
- package/dist/tests/gatewayClient.test.d.ts +2 -0
- package/dist/tests/gatewayClient.test.d.ts.map +1 -0
- package/dist/tests/gatewayClient.test.js +85 -0
- package/dist/tests/gatewayClient.test.js.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.d.ts +2 -0
- package/dist/tests/latchkeyEndpoint.test.d.ts.map +1 -0
- package/dist/tests/latchkeyEndpoint.test.js +385 -0
- package/dist/tests/latchkeyEndpoint.test.js.map +1 -0
- package/dist/tests/permissions.test.js +18 -3
- package/dist/tests/permissions.test.js.map +1 -1
- package/dist/tests/serviceRegistry.test.d.ts +2 -0
- package/dist/tests/serviceRegistry.test.d.ts.map +1 -0
- package/dist/tests/{registry.test.js → serviceRegistry.test.js} +17 -17
- package/dist/tests/serviceRegistry.test.js.map +1 -0
- package/dist/tests/servicesAgainstRecordings.test.js +3 -3
- package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
- package/dist/tests/sharedOperations.test.d.ts +2 -0
- package/dist/tests/sharedOperations.test.d.ts.map +1 -0
- package/dist/tests/sharedOperations.test.js +264 -0
- package/dist/tests/sharedOperations.test.js.map +1 -0
- package/package.json +8 -2
- package/dist/src/apiCredentialStore.d.ts.map +0 -1
- package/dist/src/apiCredentialStore.js.map +0 -1
- package/dist/src/apiCredentials.d.ts.map +0 -1
- package/dist/src/apiCredentials.js.map +0 -1
- package/dist/src/apiCredentialsSerialization.d.ts.map +0 -1
- package/dist/src/apiCredentialsSerialization.js.map +0 -1
- package/dist/src/registry.d.ts.map +0 -1
- package/dist/src/registry.js.map +0 -1
- package/dist/tests/registry.test.d.ts +0 -2
- package/dist/tests/registry.test.d.ts.map +0 -1
- package/dist/tests/registry.test.js.map +0 -1
package/dist/tests/cli.test.js
CHANGED
|
@@ -5,12 +5,12 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
7
|
import { registerCommands } from '../src/cliCommands.js';
|
|
8
|
-
import { extractUrlFromCurlArguments } from '../src/curl.js';
|
|
8
|
+
import { CurlParseError, extractUrlFromCurlArguments } from '../src/curl.js';
|
|
9
9
|
import { hasGraphicalEnvironment } from '../src/playwrightUtils.js';
|
|
10
10
|
import { EncryptedStorage } from '../src/encryptedStorage.js';
|
|
11
11
|
import { Config } from '../src/config.js';
|
|
12
|
-
import {
|
|
13
|
-
import { ApiCredentialStatus } from '../src/apiCredentials.js';
|
|
12
|
+
import { ServiceRegistry } from '../src/serviceRegistry.js';
|
|
13
|
+
import { ApiCredentialStatus } from '../src/apiCredentials/base.js';
|
|
14
14
|
import { SlackApiCredentials } from '../src/services/slack.js';
|
|
15
15
|
import { NoCurlCredentialsNotSupportedError, Service } from '../src/services/core/base.js';
|
|
16
16
|
import { RegisteredService } from '../src/services/core/registered.js';
|
|
@@ -18,7 +18,7 @@ import { GITLAB } from '../src/services/gitlab.js';
|
|
|
18
18
|
import { GITHUB } from '../src/services/github.js';
|
|
19
19
|
import { TELEGRAM } from '../src/services/telegram.js';
|
|
20
20
|
import { deleteRegisteredService, loadRegisteredServices, saveRegisteredService, } from '../src/configDataStore.js';
|
|
21
|
-
import {
|
|
21
|
+
import { loadRegisteredServicesIntoServiceRegistry } from '../src/serviceRegistry.js';
|
|
22
22
|
// Use a fixed test key for deterministic test behavior (32 bytes = 256 bits, base64 encoded)
|
|
23
23
|
const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
|
|
24
24
|
async function writeSecureFile(path, content) {
|
|
@@ -98,13 +98,13 @@ describe('extractUrlFromCurlArguments', () => {
|
|
|
98
98
|
const arguments_ = ['--header', 'Authorization: Bearer token', 'https://api.example.com'];
|
|
99
99
|
expect(extractUrlFromCurlArguments(arguments_)).toBe('https://api.example.com');
|
|
100
100
|
});
|
|
101
|
-
it('should
|
|
101
|
+
it('should throw CurlParseError when no URL is present', () => {
|
|
102
102
|
const arguments_ = ['-X', 'POST', '-H', 'Content-Type: application/json'];
|
|
103
|
-
expect(extractUrlFromCurlArguments(arguments_)).
|
|
103
|
+
expect(() => extractUrlFromCurlArguments(arguments_)).toThrow(CurlParseError);
|
|
104
104
|
});
|
|
105
|
-
it('should
|
|
105
|
+
it('should throw CurlParseError for empty arguments', () => {
|
|
106
106
|
const arguments_ = [];
|
|
107
|
-
expect(extractUrlFromCurlArguments(arguments_)).
|
|
107
|
+
expect(() => extractUrlFromCurlArguments(arguments_)).toThrow(CurlParseError);
|
|
108
108
|
});
|
|
109
109
|
it('should handle verbose flag', () => {
|
|
110
110
|
let arguments_ = ['-v', 'https://api.example.com'];
|
|
@@ -118,6 +118,16 @@ describe('extractUrlFromCurlArguments', () => {
|
|
|
118
118
|
const arguments_ = ['-k', '--compressed', '-s', '-i', 'https://api.example.com'];
|
|
119
119
|
expect(extractUrlFromCurlArguments(arguments_)).toBe('https://api.example.com');
|
|
120
120
|
});
|
|
121
|
+
it('should return the raw arg for schemeless URLs (curl defaults to http://)', () => {
|
|
122
|
+
expect(extractUrlFromCurlArguments(['www.seznam.cz'])).toBe('www.seznam.cz');
|
|
123
|
+
expect(extractUrlFromCurlArguments(['-X', 'POST', 'api.example.com/path'])).toBe('api.example.com/path');
|
|
124
|
+
});
|
|
125
|
+
it('should return null for non-http(s) schemes', () => {
|
|
126
|
+
expect(extractUrlFromCurlArguments(['ftp://example.com/'])).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
it('should propagate CurlParseError for malformed arguments', () => {
|
|
129
|
+
expect(() => extractUrlFromCurlArguments(['-H', 'no-colon-here'])).toThrow(CurlParseError);
|
|
130
|
+
});
|
|
121
131
|
});
|
|
122
132
|
describe('hasGraphicalEnvironment', () => {
|
|
123
133
|
const originalPlatform = process.platform;
|
|
@@ -257,6 +267,11 @@ describe('CLI commands with dependency injection', () => {
|
|
|
257
267
|
accountName: overrides.accountName ?? defaultConfig.accountName,
|
|
258
268
|
browserDisabled: overrides.browserDisabled ?? false,
|
|
259
269
|
countingDisabled: overrides.countingDisabled ?? false,
|
|
270
|
+
permissionsDoNotUseBuiltinSchemas: overrides.permissionsDoNotUseBuiltinSchemas ?? false,
|
|
271
|
+
passthroughUnknown: overrides.passthroughUnknown ?? false,
|
|
272
|
+
gatewayUrl: overrides.gatewayUrl ?? null,
|
|
273
|
+
gatewayListenHost: overrides.gatewayListenHost ?? 'localhost',
|
|
274
|
+
gatewayListenPort: overrides.gatewayListenPort ?? 1989,
|
|
260
275
|
checkSensitiveFilePermissions: () => undefined,
|
|
261
276
|
checkSystemPrerequisites: () => undefined,
|
|
262
277
|
};
|
|
@@ -269,7 +284,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
269
284
|
loginUrl: 'https://slack.com/signin',
|
|
270
285
|
info: 'Test info for Slack service.',
|
|
271
286
|
credentialCheckCurlArguments: ['https://slack.com/api/auth.test'],
|
|
272
|
-
checkApiCredentials: vi.fn().
|
|
287
|
+
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Valid),
|
|
273
288
|
setCredentialsExample(serviceName) {
|
|
274
289
|
return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
|
|
275
290
|
},
|
|
@@ -280,7 +295,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
280
295
|
login: vi.fn().mockResolvedValue(new SlackApiCredentials('xoxc-test-token', 'test-cookie')),
|
|
281
296
|
}),
|
|
282
297
|
};
|
|
283
|
-
const mockRegistry = new
|
|
298
|
+
const mockRegistry = new ServiceRegistry([mockSlackService]);
|
|
284
299
|
return {
|
|
285
300
|
registry: mockRegistry,
|
|
286
301
|
config: createMockConfig(),
|
|
@@ -288,6 +303,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
288
303
|
capturedArgs.push(...args);
|
|
289
304
|
return { returncode: 0, stdout: '', stderr: '' };
|
|
290
305
|
},
|
|
306
|
+
runCurlAsync: () => Promise.resolve({ returncode: 0, stdout: Buffer.from(''), stderr: '' }),
|
|
291
307
|
checkPermission: () => Promise.resolve(true),
|
|
292
308
|
confirm: () => Promise.resolve(true),
|
|
293
309
|
exit: (code) => {
|
|
@@ -300,6 +316,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
300
316
|
errorLog: (message) => {
|
|
301
317
|
errorLogs.push(message);
|
|
302
318
|
},
|
|
319
|
+
version: '0.0.0-test',
|
|
303
320
|
...overrides,
|
|
304
321
|
};
|
|
305
322
|
}
|
|
@@ -399,7 +416,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
399
416
|
loginUrl: 'https://nologin.example.com',
|
|
400
417
|
info: 'A service without browser login support.',
|
|
401
418
|
credentialCheckCurlArguments: [],
|
|
402
|
-
checkApiCredentials: vi.fn().
|
|
419
|
+
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Missing),
|
|
403
420
|
setCredentialsExample(serviceName) {
|
|
404
421
|
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
405
422
|
},
|
|
@@ -408,7 +425,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
408
425
|
},
|
|
409
426
|
};
|
|
410
427
|
const deps = createMockDependencies({
|
|
411
|
-
registry: new
|
|
428
|
+
registry: new ServiceRegistry([noLoginService]),
|
|
412
429
|
});
|
|
413
430
|
await runCommand(['services', 'list', '--viable'], deps);
|
|
414
431
|
expect(logs).toHaveLength(1);
|
|
@@ -563,7 +580,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
563
580
|
loginUrl: 'https://nologin.example.com',
|
|
564
581
|
info: 'A service without browser login support.',
|
|
565
582
|
credentialCheckCurlArguments: [],
|
|
566
|
-
checkApiCredentials: vi.fn().
|
|
583
|
+
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Missing),
|
|
567
584
|
setCredentialsExample(serviceName) {
|
|
568
585
|
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
569
586
|
},
|
|
@@ -572,7 +589,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
572
589
|
},
|
|
573
590
|
};
|
|
574
591
|
const deps = createMockDependencies({
|
|
575
|
-
registry: new
|
|
592
|
+
registry: new ServiceRegistry([noLoginService]),
|
|
576
593
|
});
|
|
577
594
|
await runCommand(['services', 'info', 'nologin'], deps);
|
|
578
595
|
const info = JSON.parse(logs[0] ?? '');
|
|
@@ -743,7 +760,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
743
760
|
const storePath = join(tempDir, 'credentials.json');
|
|
744
761
|
await writeSecureFile(storePath, '{}');
|
|
745
762
|
const deps = createMockDependencies({
|
|
746
|
-
registry: new
|
|
763
|
+
registry: new ServiceRegistry([TELEGRAM]),
|
|
747
764
|
});
|
|
748
765
|
await runCommand(['auth', 'set-nocurl', 'telegram', '123456:ABC-DEF'], deps);
|
|
749
766
|
expect(logs).toContain('Credentials stored.');
|
|
@@ -765,14 +782,14 @@ describe('CLI commands with dependency injection', () => {
|
|
|
765
782
|
});
|
|
766
783
|
it('should return error when telegram token is missing', async () => {
|
|
767
784
|
const deps = createMockDependencies({
|
|
768
|
-
registry: new
|
|
785
|
+
registry: new ServiceRegistry([TELEGRAM]),
|
|
769
786
|
});
|
|
770
787
|
await runCommand(['auth', 'set-nocurl', 'telegram'], deps);
|
|
771
788
|
expect(exitCode).toBe(1);
|
|
772
789
|
});
|
|
773
790
|
it('should return error when telegram token format is invalid', async () => {
|
|
774
791
|
const deps = createMockDependencies({
|
|
775
|
-
registry: new
|
|
792
|
+
registry: new ServiceRegistry([TELEGRAM]),
|
|
776
793
|
});
|
|
777
794
|
await runCommand(['auth', 'set-nocurl', 'telegram', 'not-a-valid-token'], deps);
|
|
778
795
|
expect(exitCode).toBe(1);
|
|
@@ -848,6 +865,38 @@ describe('CLI commands with dependency injection', () => {
|
|
|
848
865
|
await runCommand(['curl', 'https://unknown-api.example.com'], deps);
|
|
849
866
|
expect(exitCode).toBe(1);
|
|
850
867
|
});
|
|
868
|
+
it('should pass through unknown service when passthroughUnknown is enabled', async () => {
|
|
869
|
+
const deps = createMockDependencies({
|
|
870
|
+
config: createMockConfig({ passthroughUnknown: true }),
|
|
871
|
+
});
|
|
872
|
+
await runCommand(['curl', 'https://unknown-api.example.com/test'], deps);
|
|
873
|
+
expect(exitCode).toBe(0);
|
|
874
|
+
expect(capturedArgs).toEqual(['https://unknown-api.example.com/test']);
|
|
875
|
+
expect(errorLogs).toHaveLength(0);
|
|
876
|
+
});
|
|
877
|
+
it('should pass through missing credentials when passthroughUnknown is enabled', async () => {
|
|
878
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
879
|
+
await writeSecureFile(storePath, '{}');
|
|
880
|
+
const deps = createMockDependencies({
|
|
881
|
+
config: createMockConfig({ passthroughUnknown: true }),
|
|
882
|
+
});
|
|
883
|
+
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
884
|
+
expect(exitCode).toBe(0);
|
|
885
|
+
expect(capturedArgs).toEqual(['https://slack.com/api/test']);
|
|
886
|
+
expect(errorLogs).toHaveLength(0);
|
|
887
|
+
});
|
|
888
|
+
it('should still inject credentials for known services when passthroughUnknown is enabled', async () => {
|
|
889
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
890
|
+
await writeSecureFile(storePath, JSON.stringify({
|
|
891
|
+
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
892
|
+
}));
|
|
893
|
+
const deps = createMockDependencies({
|
|
894
|
+
config: createMockConfig({ passthroughUnknown: true }),
|
|
895
|
+
});
|
|
896
|
+
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
897
|
+
expect(exitCode).toBe(0);
|
|
898
|
+
expect(capturedArgs).toContain('Authorization: Bearer stored-token');
|
|
899
|
+
});
|
|
851
900
|
it('should read credentials from store and not call login', async () => {
|
|
852
901
|
const storePath = join(tempDir, 'credentials.json');
|
|
853
902
|
await writeSecureFile(storePath, JSON.stringify({
|
|
@@ -871,7 +920,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
871
920
|
getSession: vi.fn().mockReturnValue({ login: mockLogin }),
|
|
872
921
|
};
|
|
873
922
|
const deps = createMockDependencies({
|
|
874
|
-
registry: new
|
|
923
|
+
registry: new ServiceRegistry([mockSlackService]),
|
|
875
924
|
});
|
|
876
925
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
877
926
|
expect(mockLogin).not.toHaveBeenCalled();
|
|
@@ -890,7 +939,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
890
939
|
telegram: { objectType: 'telegramBot', token: '123456:ABC-DEF' },
|
|
891
940
|
}));
|
|
892
941
|
const deps = createMockDependencies({
|
|
893
|
-
registry: new
|
|
942
|
+
registry: new ServiceRegistry([TELEGRAM]),
|
|
894
943
|
});
|
|
895
944
|
await runCommand(['curl', 'https://api.telegram.org/getMe'], deps);
|
|
896
945
|
expect(capturedArgs).toEqual(['https://api.telegram.org/bot123456:ABC-DEF/getMe']);
|
|
@@ -908,7 +957,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
908
957
|
loginUrl: 'https://nologin.example.com',
|
|
909
958
|
info: 'A service without browser login support.',
|
|
910
959
|
credentialCheckCurlArguments: [],
|
|
911
|
-
checkApiCredentials: vi.fn().
|
|
960
|
+
checkApiCredentials: vi.fn().mockResolvedValue(ApiCredentialStatus.Valid),
|
|
912
961
|
setCredentialsExample(serviceName) {
|
|
913
962
|
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
914
963
|
},
|
|
@@ -918,7 +967,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
918
967
|
// No getSession - service doesn't support browser login
|
|
919
968
|
};
|
|
920
969
|
const deps = createMockDependencies({
|
|
921
|
-
registry: new
|
|
970
|
+
registry: new ServiceRegistry([noLoginService]),
|
|
922
971
|
});
|
|
923
972
|
await runCommand(['curl', 'https://nologin.example.com/api/test'], deps);
|
|
924
973
|
expect(exitCode).toBe(0);
|
|
@@ -985,7 +1034,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
985
1034
|
// No getSession - service doesn't support browser login
|
|
986
1035
|
};
|
|
987
1036
|
const deps = createMockDependencies({
|
|
988
|
-
registry: new
|
|
1037
|
+
registry: new ServiceRegistry([noLoginService]),
|
|
989
1038
|
});
|
|
990
1039
|
await runCommand(['auth', 'browser', 'nologin'], deps);
|
|
991
1040
|
expect(exitCode).toBe(1);
|
|
@@ -1042,7 +1091,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1042
1091
|
// No getSession - service doesn't support browser login
|
|
1043
1092
|
};
|
|
1044
1093
|
const deps = createMockDependencies({
|
|
1045
|
-
registry: new
|
|
1094
|
+
registry: new ServiceRegistry([nocurlService]),
|
|
1046
1095
|
});
|
|
1047
1096
|
await runCommand(['auth', 'browser', 'nocurl-only'], deps);
|
|
1048
1097
|
expect(exitCode).toBe(1);
|
|
@@ -1051,7 +1100,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1051
1100
|
describe('services register command', () => {
|
|
1052
1101
|
it('should register a new service', async () => {
|
|
1053
1102
|
const deps = createMockDependencies({
|
|
1054
|
-
registry: new
|
|
1103
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1055
1104
|
});
|
|
1056
1105
|
await runCommand([
|
|
1057
1106
|
'services',
|
|
@@ -1071,7 +1120,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1071
1120
|
});
|
|
1072
1121
|
it('should persist registration to config.json', async () => {
|
|
1073
1122
|
const deps = createMockDependencies({
|
|
1074
|
-
registry: new
|
|
1123
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1075
1124
|
});
|
|
1076
1125
|
await runCommand([
|
|
1077
1126
|
'services',
|
|
@@ -1091,7 +1140,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1091
1140
|
});
|
|
1092
1141
|
it('should reject unknown service family', async () => {
|
|
1093
1142
|
const deps = createMockDependencies({
|
|
1094
|
-
registry: new
|
|
1143
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1095
1144
|
});
|
|
1096
1145
|
await runCommand([
|
|
1097
1146
|
'services',
|
|
@@ -1107,7 +1156,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1107
1156
|
});
|
|
1108
1157
|
it('should reject duplicate service name', async () => {
|
|
1109
1158
|
const deps = createMockDependencies({
|
|
1110
|
-
registry: new
|
|
1159
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1111
1160
|
});
|
|
1112
1161
|
await runCommand([
|
|
1113
1162
|
'services',
|
|
@@ -1123,7 +1172,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1123
1172
|
});
|
|
1124
1173
|
it('should canonicalize service name to lowercase', async () => {
|
|
1125
1174
|
const deps = createMockDependencies({
|
|
1126
|
-
registry: new
|
|
1175
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1127
1176
|
});
|
|
1128
1177
|
await runCommand([
|
|
1129
1178
|
'services',
|
|
@@ -1140,7 +1189,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1140
1189
|
});
|
|
1141
1190
|
it('should convert spaces to hyphens in service name', async () => {
|
|
1142
1191
|
const deps = createMockDependencies({
|
|
1143
|
-
registry: new
|
|
1192
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1144
1193
|
});
|
|
1145
1194
|
await runCommand(['services', 'register', 'my api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1146
1195
|
expect(exitCode).toBeNull();
|
|
@@ -1149,7 +1198,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1149
1198
|
});
|
|
1150
1199
|
it('should reject service name with invalid characters', async () => {
|
|
1151
1200
|
const deps = createMockDependencies({
|
|
1152
|
-
registry: new
|
|
1201
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1153
1202
|
});
|
|
1154
1203
|
await runCommand(['services', 'register', 'my@service!', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1155
1204
|
expect(exitCode).toBe(1);
|
|
@@ -1159,7 +1208,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1159
1208
|
const storePath = join(tempDir, 'credentials.json');
|
|
1160
1209
|
await writeSecureFile(storePath, '{}');
|
|
1161
1210
|
const deps = createMockDependencies({
|
|
1162
|
-
registry: new
|
|
1211
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1163
1212
|
});
|
|
1164
1213
|
await runCommand([
|
|
1165
1214
|
'services',
|
|
@@ -1178,7 +1227,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1178
1227
|
});
|
|
1179
1228
|
it('should persist and restore loginUrl', async () => {
|
|
1180
1229
|
const deps = createMockDependencies({
|
|
1181
|
-
registry: new
|
|
1230
|
+
registry: new ServiceRegistry([GITHUB]),
|
|
1182
1231
|
});
|
|
1183
1232
|
await runCommand([
|
|
1184
1233
|
'services',
|
|
@@ -1196,7 +1245,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1196
1245
|
});
|
|
1197
1246
|
it('should reject --login-url without --service-family', async () => {
|
|
1198
1247
|
const deps = createMockDependencies({
|
|
1199
|
-
registry: new
|
|
1248
|
+
registry: new ServiceRegistry([]),
|
|
1200
1249
|
});
|
|
1201
1250
|
await runCommand([
|
|
1202
1251
|
'services',
|
|
@@ -1212,7 +1261,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1212
1261
|
});
|
|
1213
1262
|
it('should reject --login-url when service family does not support browser login', async () => {
|
|
1214
1263
|
const deps = createMockDependencies({
|
|
1215
|
-
registry: new
|
|
1264
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1216
1265
|
});
|
|
1217
1266
|
await runCommand([
|
|
1218
1267
|
'services',
|
|
@@ -1230,7 +1279,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1230
1279
|
});
|
|
1231
1280
|
it('should require --login-url when service family supports browser login', async () => {
|
|
1232
1281
|
const deps = createMockDependencies({
|
|
1233
|
-
registry: new
|
|
1282
|
+
registry: new ServiceRegistry([GITHUB]),
|
|
1234
1283
|
});
|
|
1235
1284
|
await runCommand([
|
|
1236
1285
|
'services',
|
|
@@ -1246,7 +1295,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1246
1295
|
});
|
|
1247
1296
|
it('should make registered service usable with auth set', async () => {
|
|
1248
1297
|
const deps = createMockDependencies({
|
|
1249
|
-
registry: new
|
|
1298
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1250
1299
|
});
|
|
1251
1300
|
// Register the service
|
|
1252
1301
|
await runCommand([
|
|
@@ -1269,7 +1318,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1269
1318
|
});
|
|
1270
1319
|
it('should register a service without --service-family', async () => {
|
|
1271
1320
|
const deps = createMockDependencies({
|
|
1272
|
-
registry: new
|
|
1321
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1273
1322
|
});
|
|
1274
1323
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1275
1324
|
expect(exitCode).toBeNull();
|
|
@@ -1281,7 +1330,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1281
1330
|
});
|
|
1282
1331
|
it('should persist registration without service family to config.json', async () => {
|
|
1283
1332
|
const deps = createMockDependencies({
|
|
1284
|
-
registry: new
|
|
1333
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1285
1334
|
});
|
|
1286
1335
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1287
1336
|
const configPath = deps.config.configPath;
|
|
@@ -1294,7 +1343,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1294
1343
|
const storePath = join(tempDir, 'credentials.json');
|
|
1295
1344
|
await writeSecureFile(storePath, '{}');
|
|
1296
1345
|
const deps = createMockDependencies({
|
|
1297
|
-
registry: new
|
|
1346
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1298
1347
|
});
|
|
1299
1348
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1300
1349
|
logs = [];
|
|
@@ -1305,7 +1354,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1305
1354
|
});
|
|
1306
1355
|
it('should make service without family usable with auth set and curl', async () => {
|
|
1307
1356
|
const deps = createMockDependencies({
|
|
1308
|
-
registry: new
|
|
1357
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1309
1358
|
});
|
|
1310
1359
|
// Register the service without family
|
|
1311
1360
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
@@ -1327,7 +1376,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1327
1376
|
});
|
|
1328
1377
|
it('should reject browser login for service without family', async () => {
|
|
1329
1378
|
const deps = createMockDependencies({
|
|
1330
|
-
registry: new
|
|
1379
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1331
1380
|
});
|
|
1332
1381
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1333
1382
|
logs = [];
|
|
@@ -1339,7 +1388,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1339
1388
|
});
|
|
1340
1389
|
it('should reject set-nocurl for service without family', async () => {
|
|
1341
1390
|
const deps = createMockDependencies({
|
|
1342
|
-
registry: new
|
|
1391
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1343
1392
|
});
|
|
1344
1393
|
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
1345
1394
|
logs = [];
|
|
@@ -1351,7 +1400,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1351
1400
|
});
|
|
1352
1401
|
it('should make registered service usable with curl', async () => {
|
|
1353
1402
|
const deps = createMockDependencies({
|
|
1354
|
-
registry: new
|
|
1403
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1355
1404
|
});
|
|
1356
1405
|
// Register the service
|
|
1357
1406
|
await runCommand([
|
|
@@ -1383,7 +1432,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1383
1432
|
describe('services deregister command', () => {
|
|
1384
1433
|
it('should deregister a registered service', async () => {
|
|
1385
1434
|
const deps = createMockDependencies({
|
|
1386
|
-
registry: new
|
|
1435
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1387
1436
|
});
|
|
1388
1437
|
// Register a service first
|
|
1389
1438
|
await runCommand([
|
|
@@ -1403,7 +1452,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1403
1452
|
});
|
|
1404
1453
|
it('should remove service from config.json', async () => {
|
|
1405
1454
|
const deps = createMockDependencies({
|
|
1406
|
-
registry: new
|
|
1455
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1407
1456
|
});
|
|
1408
1457
|
await runCommand([
|
|
1409
1458
|
'services',
|
|
@@ -1434,7 +1483,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1434
1483
|
});
|
|
1435
1484
|
it('should reject deregistering when credentials still exist', async () => {
|
|
1436
1485
|
const deps = createMockDependencies({
|
|
1437
|
-
registry: new
|
|
1486
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1438
1487
|
});
|
|
1439
1488
|
// Register a service
|
|
1440
1489
|
await runCommand([
|
|
@@ -1467,7 +1516,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1467
1516
|
});
|
|
1468
1517
|
it('should allow deregistering after credentials are cleared', async () => {
|
|
1469
1518
|
const deps = createMockDependencies({
|
|
1470
|
-
registry: new
|
|
1519
|
+
registry: new ServiceRegistry([GITLAB]),
|
|
1471
1520
|
});
|
|
1472
1521
|
// Register
|
|
1473
1522
|
await runCommand([
|
|
@@ -1500,6 +1549,143 @@ describe('CLI commands with dependency injection', () => {
|
|
|
1500
1549
|
expect(logs).toContain("Service 'my-gitlab' deregistered.");
|
|
1501
1550
|
});
|
|
1502
1551
|
});
|
|
1552
|
+
describe('gateway mode (LATCHKEY_GATEWAY)', () => {
|
|
1553
|
+
const GATEWAY_URL = 'http://localhost:9000';
|
|
1554
|
+
const originalFetch = globalThis.fetch;
|
|
1555
|
+
afterEach(() => {
|
|
1556
|
+
globalThis.fetch = originalFetch;
|
|
1557
|
+
});
|
|
1558
|
+
function makeFetchMock(response) {
|
|
1559
|
+
const fetchMock = vi.fn().mockResolvedValue(response);
|
|
1560
|
+
globalThis.fetch = fetchMock;
|
|
1561
|
+
return fetchMock;
|
|
1562
|
+
}
|
|
1563
|
+
it('forwards `services list` to the gateway /latchkey endpoint', async () => {
|
|
1564
|
+
const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: ['slack', 'github'] }), { status: 200 }));
|
|
1565
|
+
const deps = createMockDependencies({
|
|
1566
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1567
|
+
});
|
|
1568
|
+
await runCommand(['services', 'list', '--builtin'], deps);
|
|
1569
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1570
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
1571
|
+
expect(url).toBe(`${GATEWAY_URL}/latchkey`);
|
|
1572
|
+
expect(init.method).toBe('POST');
|
|
1573
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
1574
|
+
command: 'services list',
|
|
1575
|
+
params: { builtin: true },
|
|
1576
|
+
});
|
|
1577
|
+
expect(logs).toHaveLength(1);
|
|
1578
|
+
expect(JSON.parse(logs[0] ?? '')).toEqual(['slack', 'github']);
|
|
1579
|
+
});
|
|
1580
|
+
it('forwards `services info` to the gateway /latchkey endpoint', async () => {
|
|
1581
|
+
const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: { type: 'built-in' } }), { status: 200 }));
|
|
1582
|
+
const deps = createMockDependencies({
|
|
1583
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1584
|
+
});
|
|
1585
|
+
await runCommand(['services', 'info', 'slack'], deps);
|
|
1586
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1587
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
1588
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
1589
|
+
command: 'services info',
|
|
1590
|
+
params: { serviceName: 'slack' },
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
it('forwards `auth list` to the gateway /latchkey endpoint', async () => {
|
|
1594
|
+
const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: { slack: { credentialType: 'slack' } } }), {
|
|
1595
|
+
status: 200,
|
|
1596
|
+
}));
|
|
1597
|
+
const deps = createMockDependencies({
|
|
1598
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1599
|
+
});
|
|
1600
|
+
await runCommand(['auth', 'list'], deps);
|
|
1601
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
1602
|
+
expect(JSON.parse(init.body)).toEqual({ command: 'auth list' });
|
|
1603
|
+
expect(JSON.parse(logs[0] ?? '')).toEqual({
|
|
1604
|
+
slack: { credentialType: 'slack' },
|
|
1605
|
+
});
|
|
1606
|
+
});
|
|
1607
|
+
it('forwards `auth browser` to the gateway /latchkey endpoint', async () => {
|
|
1608
|
+
const fetchMock = makeFetchMock(new Response(JSON.stringify({ result: null }), { status: 200 }));
|
|
1609
|
+
const deps = createMockDependencies({
|
|
1610
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1611
|
+
});
|
|
1612
|
+
await runCommand(['auth', 'browser', 'slack'], deps);
|
|
1613
|
+
const [, init] = fetchMock.mock.calls[0];
|
|
1614
|
+
expect(JSON.parse(init.body)).toEqual({
|
|
1615
|
+
command: 'auth browser',
|
|
1616
|
+
params: { serviceName: 'slack' },
|
|
1617
|
+
});
|
|
1618
|
+
expect(logs).toContain('Done');
|
|
1619
|
+
});
|
|
1620
|
+
it('forwards `auth browser-prepare` and reports `Already prepared.` when the gateway says so', async () => {
|
|
1621
|
+
makeFetchMock(new Response(JSON.stringify({ result: { alreadyPrepared: true } }), { status: 200 }));
|
|
1622
|
+
const deps = createMockDependencies({
|
|
1623
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1624
|
+
});
|
|
1625
|
+
await runCommand(['auth', 'browser-prepare', 'slack'], deps);
|
|
1626
|
+
expect(logs).toContain('Already prepared.');
|
|
1627
|
+
});
|
|
1628
|
+
it.each([
|
|
1629
|
+
['services list', ['services', 'list']],
|
|
1630
|
+
['services info', ['services', 'info', 'foo']],
|
|
1631
|
+
['auth list', ['auth', 'list']],
|
|
1632
|
+
['auth browser', ['auth', 'browser', 'slack']],
|
|
1633
|
+
['auth browser-prepare', ['auth', 'browser-prepare', 'slack']],
|
|
1634
|
+
])('reports gateway errors on stderr (not stdout) for `%s`', async (_name, argv) => {
|
|
1635
|
+
makeFetchMock(new Response(JSON.stringify({ error: 'Unknown service: foo.' }), { status: 400 }));
|
|
1636
|
+
const deps = createMockDependencies({
|
|
1637
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1638
|
+
});
|
|
1639
|
+
await runCommand(argv, deps);
|
|
1640
|
+
expect(exitCode).toBe(1);
|
|
1641
|
+
expect(errorLogs.some((message) => message.includes('Unknown service: foo.'))).toBe(true);
|
|
1642
|
+
// The error message must never be printed to stdout — that would
|
|
1643
|
+
// interleave into JSON output consumed by callers.
|
|
1644
|
+
expect(logs.join('\n')).not.toContain('Unknown service: foo.');
|
|
1645
|
+
expect(logs).toEqual([]);
|
|
1646
|
+
});
|
|
1647
|
+
it('rewrites the curl target URL to the gateway /gateway endpoint', async () => {
|
|
1648
|
+
const fetchMock = vi.fn();
|
|
1649
|
+
globalThis.fetch = fetchMock;
|
|
1650
|
+
const deps = createMockDependencies({
|
|
1651
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1652
|
+
});
|
|
1653
|
+
await runCommand(['curl', '-X', 'GET', 'https://slack.com/api/auth.test'], deps);
|
|
1654
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1655
|
+
expect(capturedArgs).toEqual([
|
|
1656
|
+
'-X',
|
|
1657
|
+
'GET',
|
|
1658
|
+
`${GATEWAY_URL}/gateway/https://slack.com/api/auth.test`,
|
|
1659
|
+
]);
|
|
1660
|
+
});
|
|
1661
|
+
it('errors when `latchkey curl` has no URL to rewrite', async () => {
|
|
1662
|
+
const deps = createMockDependencies({
|
|
1663
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1664
|
+
});
|
|
1665
|
+
await runCommand(['curl', '-X', 'GET'], deps);
|
|
1666
|
+
expect(exitCode).toBe(1);
|
|
1667
|
+
expect(capturedArgs).toEqual([]);
|
|
1668
|
+
});
|
|
1669
|
+
it.each([
|
|
1670
|
+
[
|
|
1671
|
+
'services register',
|
|
1672
|
+
['services', 'register', 'my-gitlab', '--base-api-url', 'https://gitlab.example.com'],
|
|
1673
|
+
],
|
|
1674
|
+
['services deregister', ['services', 'deregister', 'my-gitlab']],
|
|
1675
|
+
['auth clear', ['auth', 'clear', 'slack']],
|
|
1676
|
+
['auth set', ['auth', 'set', 'slack', '-H', 'Authorization: Bearer xoxb-test']],
|
|
1677
|
+
['auth set-nocurl', ['auth', 'set-nocurl', 'slack', 'x']],
|
|
1678
|
+
['gateway', ['gateway']],
|
|
1679
|
+
['ensure-browser', ['ensure-browser']],
|
|
1680
|
+
])('refuses to run `%s` in gateway mode', async (_name, argv) => {
|
|
1681
|
+
const deps = createMockDependencies({
|
|
1682
|
+
config: createMockConfig({ gatewayUrl: GATEWAY_URL }),
|
|
1683
|
+
});
|
|
1684
|
+
await runCommand(argv, deps);
|
|
1685
|
+
expect(exitCode).toBe(1);
|
|
1686
|
+
expect(errorLogs.some((message) => message.includes('LATCHKEY_GATEWAY'))).toBe(true);
|
|
1687
|
+
});
|
|
1688
|
+
});
|
|
1503
1689
|
});
|
|
1504
1690
|
describe('registeredServiceStore', () => {
|
|
1505
1691
|
let tempDir;
|
|
@@ -1544,8 +1730,8 @@ describe('registeredServiceStore', () => {
|
|
|
1544
1730
|
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1545
1731
|
serviceFamily: 'gitlab',
|
|
1546
1732
|
});
|
|
1547
|
-
const registry = new
|
|
1548
|
-
|
|
1733
|
+
const registry = new ServiceRegistry([GITLAB]);
|
|
1734
|
+
loadRegisteredServicesIntoServiceRegistry(configPath, registry);
|
|
1549
1735
|
const service = registry.getByName('my-gitlab');
|
|
1550
1736
|
expect(service).not.toBeNull();
|
|
1551
1737
|
expect(service.baseApiUrls).toEqual(['https://gitlab.mycompany.com/api/']);
|
|
@@ -1557,8 +1743,8 @@ describe('registeredServiceStore', () => {
|
|
|
1557
1743
|
serviceFamily: 'gitlab',
|
|
1558
1744
|
loginUrl: 'https://gitlab.mycompany.com/users/sign_in',
|
|
1559
1745
|
});
|
|
1560
|
-
const registry = new
|
|
1561
|
-
|
|
1746
|
+
const registry = new ServiceRegistry([GITLAB]);
|
|
1747
|
+
loadRegisteredServicesIntoServiceRegistry(configPath, registry);
|
|
1562
1748
|
const service = registry.getByName('my-gitlab');
|
|
1563
1749
|
expect(service).not.toBeNull();
|
|
1564
1750
|
expect(service.loginUrl).toBe('https://gitlab.mycompany.com/users/sign_in');
|
|
@@ -1568,8 +1754,8 @@ describe('registeredServiceStore', () => {
|
|
|
1568
1754
|
saveRegisteredService(configPath, 'my-api', {
|
|
1569
1755
|
baseApiUrl: 'https://api.example.com/',
|
|
1570
1756
|
});
|
|
1571
|
-
const registry = new
|
|
1572
|
-
|
|
1757
|
+
const registry = new ServiceRegistry([GITLAB]);
|
|
1758
|
+
loadRegisteredServicesIntoServiceRegistry(configPath, registry);
|
|
1573
1759
|
const service = registry.getByName('my-api');
|
|
1574
1760
|
expect(service).not.toBeNull();
|
|
1575
1761
|
expect(service.baseApiUrls).toEqual(['https://api.example.com/']);
|
|
@@ -1582,8 +1768,8 @@ describe('registeredServiceStore', () => {
|
|
|
1582
1768
|
baseApiUrl: 'https://unknown.example.com/api/',
|
|
1583
1769
|
serviceFamily: 'nonexistent',
|
|
1584
1770
|
});
|
|
1585
|
-
const registry = new
|
|
1586
|
-
|
|
1771
|
+
const registry = new ServiceRegistry([GITLAB]);
|
|
1772
|
+
loadRegisteredServicesIntoServiceRegistry(configPath, registry);
|
|
1587
1773
|
expect(registry.getByName('my-unknown')).toBeNull();
|
|
1588
1774
|
});
|
|
1589
1775
|
it('should delete a registered service from config', () => {
|