latchkey 1.0.1 → 2.1.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 +66 -8
- package/dist/integrations/SKILL.md +15 -3
- package/dist/package.json +2 -2
- package/dist/scripts/codegen.js +1 -1
- package/dist/scripts/codegen.js.map +1 -1
- package/dist/src/apiCredentialStore.d.ts +1 -1
- package/dist/src/apiCredentialStore.d.ts.map +1 -1
- package/dist/src/apiCredentialStore.js +1 -1
- package/dist/src/apiCredentialStore.js.map +1 -1
- package/dist/src/apiCredentials.d.ts +13 -115
- package/dist/src/apiCredentials.d.ts.map +1 -1
- package/dist/src/apiCredentials.js +10 -101
- package/dist/src/apiCredentials.js.map +1 -1
- package/dist/src/apiCredentialsSerialization.d.ts +119 -0
- package/dist/src/apiCredentialsSerialization.d.ts.map +1 -0
- package/dist/src/apiCredentialsSerialization.js +90 -0
- package/dist/src/apiCredentialsSerialization.js.map +1 -0
- package/dist/src/browserConfig.d.ts +2 -33
- package/dist/src/browserConfig.d.ts.map +1 -1
- package/dist/src/browserConfig.js +6 -81
- package/dist/src/browserConfig.js.map +1 -1
- package/dist/src/cli.js +22 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliCommands.d.ts +0 -1
- package/dist/src/cliCommands.d.ts.map +1 -1
- package/dist/src/cliCommands.js +148 -44
- package/dist/src/cliCommands.js.map +1 -1
- package/dist/src/config.d.ts +4 -3
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +17 -23
- package/dist/src/config.js.map +1 -1
- package/dist/src/configDataStore.d.ts +43 -0
- package/dist/src/configDataStore.d.ts.map +1 -0
- package/dist/src/configDataStore.js +108 -0
- package/dist/src/configDataStore.js.map +1 -0
- package/dist/src/curl.d.ts +10 -0
- package/dist/src/curl.d.ts.map +1 -1
- package/dist/src/curl.js +88 -0
- package/dist/src/curl.js.map +1 -1
- package/dist/src/encryptedStorage.d.ts +9 -0
- package/dist/src/encryptedStorage.d.ts.map +1 -1
- package/dist/src/encryptedStorage.js +12 -0
- package/dist/src/encryptedStorage.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/migrations.d.ts +9 -0
- package/dist/src/migrations.d.ts.map +1 -0
- package/dist/src/migrations.js +77 -0
- package/dist/src/migrations.js.map +1 -0
- package/dist/src/oauthUtils.d.ts +4 -1
- package/dist/src/oauthUtils.d.ts.map +1 -1
- package/dist/src/oauthUtils.js +1 -1
- package/dist/src/oauthUtils.js.map +1 -1
- package/dist/src/playwrightUtils.d.ts +2 -2
- package/dist/src/playwrightUtils.d.ts.map +1 -1
- package/dist/src/playwrightUtils.js +4 -4
- package/dist/src/playwrightUtils.js.map +1 -1
- package/dist/src/registeredService.d.ts +20 -0
- package/dist/src/registeredService.d.ts.map +1 -0
- package/dist/src/registeredService.js +34 -0
- package/dist/src/registeredService.js.map +1 -0
- package/dist/src/registeredServiceStore.d.ts +24 -0
- package/dist/src/registeredServiceStore.d.ts.map +1 -0
- package/dist/src/registeredServiceStore.js +70 -0
- package/dist/src/registeredServiceStore.js.map +1 -0
- package/dist/src/registry.d.ts +11 -1
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +70 -6
- package/dist/src/registry.js.map +1 -1
- package/dist/src/services/aws.d.ts +44 -0
- package/dist/src/services/aws.d.ts.map +1 -0
- package/dist/src/services/aws.js +237 -0
- package/dist/src/services/aws.js.map +1 -0
- package/dist/src/services/base.d.ts +14 -0
- package/dist/src/services/base.d.ts.map +1 -1
- package/dist/src/services/base.js +23 -11
- package/dist/src/services/base.js.map +1 -1
- package/dist/src/services/calendly.d.ts +12 -0
- package/dist/src/services/calendly.d.ts.map +1 -0
- package/dist/src/services/calendly.js +18 -0
- package/dist/src/services/calendly.js.map +1 -0
- package/dist/src/services/core/base.d.ts +141 -0
- package/dist/src/services/core/base.d.ts.map +1 -0
- package/dist/src/services/core/base.js +189 -0
- package/dist/src/services/core/base.js.map +1 -0
- package/dist/src/services/core/registered.d.ts +24 -0
- package/dist/src/services/core/registered.d.ts.map +1 -0
- package/dist/src/services/core/registered.js +53 -0
- package/dist/src/services/core/registered.js.map +1 -0
- package/dist/src/services/discord.d.ts +2 -1
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +4 -1
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +3 -2
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +6 -1
- package/dist/src/services/dropbox.js.map +1 -1
- package/dist/src/services/figma.d.ts +12 -0
- package/dist/src/services/figma.d.ts.map +1 -0
- package/dist/src/services/figma.js +14 -0
- package/dist/src/services/figma.js.map +1 -0
- package/dist/src/services/github.d.ts +2 -1
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +4 -1
- package/dist/src/services/github.js.map +1 -1
- package/dist/src/services/gitlab.d.ts +12 -0
- package/dist/src/services/gitlab.d.ts.map +1 -0
- package/dist/src/services/gitlab.js +14 -0
- package/dist/src/services/gitlab.js.map +1 -0
- package/dist/src/services/google/analytics.d.ts +11 -0
- package/dist/src/services/google/analytics.d.ts.map +1 -0
- package/dist/src/services/google/analytics.js +22 -0
- package/dist/src/services/google/analytics.js.map +1 -0
- package/dist/src/services/google/base.d.ts +73 -0
- package/dist/src/services/google/base.d.ts.map +1 -0
- package/dist/src/services/google/base.js +323 -0
- package/dist/src/services/google/base.js.map +1 -0
- package/dist/src/services/google/calendar.d.ts +11 -0
- package/dist/src/services/google/calendar.d.ts.map +1 -0
- package/dist/src/services/google/calendar.js +22 -0
- package/dist/src/services/google/calendar.js.map +1 -0
- package/dist/src/services/google/directions.d.ts +14 -0
- package/dist/src/services/google/directions.d.ts.map +1 -0
- package/dist/src/services/google/directions.js +49 -0
- package/dist/src/services/google/directions.js.map +1 -0
- package/dist/src/services/google/docs.d.ts +11 -0
- package/dist/src/services/google/docs.d.ts.map +1 -0
- package/dist/src/services/google/docs.js +19 -0
- package/dist/src/services/google/docs.js.map +1 -0
- package/dist/src/services/google/drive.d.ts +11 -0
- package/dist/src/services/google/drive.d.ts.map +1 -0
- package/dist/src/services/google/drive.js +19 -0
- package/dist/src/services/google/drive.js.map +1 -0
- package/dist/src/services/google/gmail.d.ts +11 -0
- package/dist/src/services/google/gmail.d.ts.map +1 -0
- package/dist/src/services/google/gmail.js +24 -0
- package/dist/src/services/google/gmail.js.map +1 -0
- package/dist/src/services/google/maps.d.ts +39 -0
- package/dist/src/services/google/maps.d.ts.map +1 -0
- package/dist/src/services/google/maps.js +94 -0
- package/dist/src/services/google/maps.js.map +1 -0
- package/dist/src/services/google/people.d.ts +11 -0
- package/dist/src/services/google/people.d.ts.map +1 -0
- package/dist/src/services/google/people.js +22 -0
- package/dist/src/services/google/people.js.map +1 -0
- package/dist/src/services/google/sheets.d.ts +11 -0
- package/dist/src/services/google/sheets.d.ts.map +1 -0
- package/dist/src/services/google/sheets.js +19 -0
- package/dist/src/services/google/sheets.js.map +1 -0
- package/dist/src/services/googleAnalytics.d.ts +11 -0
- package/dist/src/services/googleAnalytics.d.ts.map +1 -0
- package/dist/src/services/googleAnalytics.js +18 -0
- package/dist/src/services/googleAnalytics.js.map +1 -0
- package/dist/src/services/googleMaps.d.ts +12 -0
- package/dist/src/services/googleMaps.d.ts.map +1 -0
- package/dist/src/services/googleMaps.js +17 -0
- package/dist/src/services/googleMaps.js.map +1 -0
- package/dist/src/services/index.d.ts +21 -3
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +21 -3
- 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 +4 -1
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/mailchimp.d.ts +3 -2
- package/dist/src/services/mailchimp.d.ts.map +1 -1
- package/dist/src/services/mailchimp.js +5 -4
- package/dist/src/services/mailchimp.js.map +1 -1
- package/dist/src/services/notion.d.ts +2 -1
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +4 -1
- package/dist/src/services/notion.js.map +1 -1
- package/dist/src/services/sentry.d.ts +14 -0
- package/dist/src/services/sentry.d.ts.map +1 -0
- package/dist/src/services/sentry.js +43 -0
- package/dist/src/services/sentry.js.map +1 -0
- package/dist/src/services/slack.d.ts +31 -2
- package/dist/src/services/slack.d.ts.map +1 -1
- package/dist/src/services/slack.js +46 -3
- package/dist/src/services/slack.js.map +1 -1
- package/dist/src/services/stripe.d.ts +12 -0
- package/dist/src/services/stripe.d.ts.map +1 -0
- package/dist/src/services/stripe.js +14 -0
- package/dist/src/services/stripe.js.map +1 -0
- package/dist/src/services/telegram.d.ts +40 -0
- package/dist/src/services/telegram.d.ts.map +1 -0
- package/dist/src/services/telegram.js +73 -0
- package/dist/src/services/telegram.js.map +1 -0
- package/dist/src/services/yelp.d.ts +12 -0
- package/dist/src/services/yelp.d.ts.map +1 -0
- package/dist/src/services/yelp.js +16 -0
- package/dist/src/services/yelp.js.map +1 -0
- package/dist/src/services/zoom.d.ts +12 -0
- package/dist/src/services/zoom.d.ts.map +1 -0
- package/dist/src/services/zoom.js +18 -0
- package/dist/src/services/zoom.js.map +1 -0
- package/dist/tests/apiCredentialStore.test.js +2 -19
- package/dist/tests/apiCredentialStore.test.js.map +1 -1
- package/dist/tests/apiCredentials.test.js +139 -178
- package/dist/tests/apiCredentials.test.js.map +1 -1
- package/dist/tests/cli.test.js +755 -255
- package/dist/tests/cli.test.js.map +1 -1
- package/dist/tests/encryptedStorage.test.js +0 -4
- package/dist/tests/encryptedStorage.test.js.map +1 -1
- package/dist/tests/encryptedStorageKeyGeneration.test.d.ts +2 -0
- package/dist/tests/encryptedStorageKeyGeneration.test.d.ts.map +1 -0
- package/dist/tests/encryptedStorageKeyGeneration.test.js +22 -0
- package/dist/tests/encryptedStorageKeyGeneration.test.js.map +1 -0
- package/dist/tests/encryption.test.js +3 -35
- package/dist/tests/encryption.test.js.map +1 -1
- package/dist/tests/migrations.test.d.ts +2 -0
- package/dist/tests/migrations.test.d.ts.map +1 -0
- package/dist/tests/migrations.test.js +164 -0
- package/dist/tests/migrations.test.js.map +1 -0
- package/dist/tests/playwrightDownload.test.js +2 -65
- package/dist/tests/playwrightDownload.test.js.map +1 -1
- package/dist/tests/registry.test.js +147 -75
- package/dist/tests/registry.test.js.map +1 -1
- package/dist/tests/servicesAgainstRecordings.test.js +2 -2
- package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
- package/package.json +2 -2
package/dist/tests/cli.test.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { registerCommands } from '../src/cliCommands.js';
|
|
8
|
+
import { extractUrlFromCurlArguments } from '../src/curl.js';
|
|
9
9
|
import { EncryptedStorage } from '../src/encryptedStorage.js';
|
|
10
10
|
import { Config } from '../src/config.js';
|
|
11
11
|
import { Registry } from '../src/registry.js';
|
|
12
|
-
import {
|
|
12
|
+
import { ApiCredentialStatus } from '../src/apiCredentials.js';
|
|
13
|
+
import { SlackApiCredentials } from '../src/services/slack.js';
|
|
14
|
+
import { NoCurlCredentialsNotSupportedError, Service } from '../src/services/core/base.js';
|
|
15
|
+
import { RegisteredService } from '../src/services/core/registered.js';
|
|
16
|
+
import { GITLAB } from '../src/services/gitlab.js';
|
|
17
|
+
import { GITHUB } from '../src/services/github.js';
|
|
18
|
+
import { TELEGRAM } from '../src/services/telegram.js';
|
|
19
|
+
import { deleteRegisteredService, loadRegisteredServices, saveRegisteredService, } from '../src/configDataStore.js';
|
|
20
|
+
import { loadRegisteredServicesIntoRegistry } from '../src/registry.js';
|
|
13
21
|
// Use a fixed test key for deterministic test behavior (32 bytes = 256 bits, base64 encoded)
|
|
14
22
|
const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
|
|
15
23
|
function writeSecureFile(path, content) {
|
|
@@ -115,10 +123,18 @@ describe('CLI commands with dependency injection', () => {
|
|
|
115
123
|
let exitCode;
|
|
116
124
|
function createMockConfig(overrides = {}) {
|
|
117
125
|
const defaultConfig = new Config(() => undefined);
|
|
126
|
+
const directory = overrides.directory ?? tempDir;
|
|
118
127
|
return {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
directory,
|
|
129
|
+
get credentialStorePath() {
|
|
130
|
+
return join(directory, 'credentials.json');
|
|
131
|
+
},
|
|
132
|
+
get browserStatePath() {
|
|
133
|
+
return join(directory, 'browser_state.json');
|
|
134
|
+
},
|
|
135
|
+
get configPath() {
|
|
136
|
+
return join(directory, 'config.json');
|
|
137
|
+
},
|
|
122
138
|
curlCommand: overrides.curlCommand ?? defaultConfig.curlCommand,
|
|
123
139
|
encryptionKeyOverride: overrides.encryptionKeyOverride ?? TEST_ENCRYPTION_KEY,
|
|
124
140
|
serviceName: overrides.serviceName ?? defaultConfig.serviceName,
|
|
@@ -137,6 +153,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
137
153
|
info: 'Test info for Slack service.',
|
|
138
154
|
credentialCheckCurlArguments: ['https://slack.com/api/auth.test'],
|
|
139
155
|
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
|
|
156
|
+
setCredentialsExample(serviceName) {
|
|
157
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
|
|
158
|
+
},
|
|
159
|
+
getCredentialsNoCurl() {
|
|
160
|
+
throw new NoCurlCredentialsNotSupportedError('slack');
|
|
161
|
+
},
|
|
140
162
|
getSession: vi.fn().mockReturnValue({
|
|
141
163
|
login: vi.fn().mockResolvedValue(new SlackApiCredentials('xoxc-test-token', 'test-cookie')),
|
|
142
164
|
}),
|
|
@@ -195,19 +217,39 @@ describe('CLI commands with dependency injection', () => {
|
|
|
195
217
|
const services = JSON.parse(logs[0] ?? '');
|
|
196
218
|
expect(services).toContain('slack');
|
|
197
219
|
});
|
|
220
|
+
it('should include registered services by default', async () => {
|
|
221
|
+
const registeredService = new RegisteredService('my-gitlab', 'https://gitlab.example.com');
|
|
222
|
+
const deps = createMockDependencies();
|
|
223
|
+
deps.registry.addService(registeredService);
|
|
224
|
+
await runCommand(['services', 'list'], deps);
|
|
225
|
+
expect(logs).toHaveLength(1);
|
|
226
|
+
const services = JSON.parse(logs[0] ?? '');
|
|
227
|
+
expect(services).toContain('slack');
|
|
228
|
+
expect(services).toContain('my-gitlab');
|
|
229
|
+
});
|
|
230
|
+
it('should exclude registered services with --built-in-only', async () => {
|
|
231
|
+
const registeredService = new RegisteredService('my-gitlab', 'https://gitlab.example.com');
|
|
232
|
+
const deps = createMockDependencies();
|
|
233
|
+
deps.registry.addService(registeredService);
|
|
234
|
+
await runCommand(['services', 'list', '--built-in-only'], deps);
|
|
235
|
+
expect(logs).toHaveLength(1);
|
|
236
|
+
const services = JSON.parse(logs[0] ?? '');
|
|
237
|
+
expect(services).toContain('slack');
|
|
238
|
+
expect(services).not.toContain('my-gitlab');
|
|
239
|
+
});
|
|
198
240
|
});
|
|
199
241
|
describe('services info command', () => {
|
|
200
242
|
it('should show login options, credentials status, and developer notes', async () => {
|
|
201
243
|
const storePath = join(tempDir, 'credentials.json');
|
|
202
244
|
writeSecureFile(storePath, '{}');
|
|
203
|
-
const deps = createMockDependencies(
|
|
204
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
205
|
-
});
|
|
245
|
+
const deps = createMockDependencies();
|
|
206
246
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
207
247
|
expect(logs).toHaveLength(1);
|
|
208
248
|
const info = JSON.parse(logs[0] ?? '');
|
|
249
|
+
expect(info.type).toBe('built-in');
|
|
209
250
|
expect(info.authOptions).toEqual(['browser', 'set']);
|
|
210
251
|
expect(info.credentialStatus).toBe('missing');
|
|
252
|
+
expect(info.setCredentialsExample).toBe('latchkey auth set slack -H "Authorization: Bearer xoxb-your-token"');
|
|
211
253
|
expect(info.developerNotes).toBe('Test info for Slack service.');
|
|
212
254
|
});
|
|
213
255
|
it('should show auth set only for services without browser login', async () => {
|
|
@@ -221,10 +263,15 @@ describe('CLI commands with dependency injection', () => {
|
|
|
221
263
|
info: 'A service without browser login support.',
|
|
222
264
|
credentialCheckCurlArguments: [],
|
|
223
265
|
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Missing),
|
|
266
|
+
setCredentialsExample(serviceName) {
|
|
267
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
268
|
+
},
|
|
269
|
+
getCredentialsNoCurl() {
|
|
270
|
+
throw new NoCurlCredentialsNotSupportedError('nologin');
|
|
271
|
+
},
|
|
224
272
|
};
|
|
225
273
|
const deps = createMockDependencies({
|
|
226
274
|
registry: new Registry([noLoginService]),
|
|
227
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
228
275
|
});
|
|
229
276
|
await runCommand(['services', 'info', 'nologin'], deps);
|
|
230
277
|
const info = JSON.parse(logs[0] ?? '');
|
|
@@ -234,7 +281,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
234
281
|
const storePath = join(tempDir, 'credentials.json');
|
|
235
282
|
writeSecureFile(storePath, '{}');
|
|
236
283
|
const deps = createMockDependencies({
|
|
237
|
-
config: createMockConfig({
|
|
284
|
+
config: createMockConfig({ browserDisabled: true }),
|
|
238
285
|
});
|
|
239
286
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
240
287
|
const info = JSON.parse(logs[0] ?? '');
|
|
@@ -245,9 +292,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
245
292
|
writeSecureFile(storePath, JSON.stringify({
|
|
246
293
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
247
294
|
}));
|
|
248
|
-
const deps = createMockDependencies(
|
|
249
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
250
|
-
});
|
|
295
|
+
const deps = createMockDependencies();
|
|
251
296
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
252
297
|
const info = JSON.parse(logs[0] ?? '');
|
|
253
298
|
expect(info.credentialStatus).toBe('valid');
|
|
@@ -256,7 +301,17 @@ describe('CLI commands with dependency injection', () => {
|
|
|
256
301
|
const deps = createMockDependencies();
|
|
257
302
|
await runCommand(['services', 'info', 'unknown-service'], deps);
|
|
258
303
|
expect(exitCode).toBe(1);
|
|
259
|
-
expect(errorLogs.
|
|
304
|
+
expect(errorLogs.length).toBeGreaterThan(0);
|
|
305
|
+
});
|
|
306
|
+
it('should show type as registered for registered services', async () => {
|
|
307
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
308
|
+
writeSecureFile(storePath, '{}');
|
|
309
|
+
const registeredService = new RegisteredService('my-gitlab', 'https://gitlab.example.com');
|
|
310
|
+
const deps = createMockDependencies();
|
|
311
|
+
deps.registry.addService(registeredService);
|
|
312
|
+
await runCommand(['services', 'info', 'my-gitlab'], deps);
|
|
313
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
314
|
+
expect(info.type).toBe('user-registered');
|
|
260
315
|
});
|
|
261
316
|
});
|
|
262
317
|
describe('clear command', () => {
|
|
@@ -265,37 +320,16 @@ describe('CLI commands with dependency injection', () => {
|
|
|
265
320
|
writeSecureFile(storePath, JSON.stringify({
|
|
266
321
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
267
322
|
}));
|
|
268
|
-
const deps = createMockDependencies(
|
|
269
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
270
|
-
});
|
|
323
|
+
const deps = createMockDependencies();
|
|
271
324
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
272
|
-
expect(logs.some((log) => log.includes('have been cleared'))).toBe(true);
|
|
273
325
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
274
326
|
expect(storedData.slack).toBeUndefined();
|
|
275
327
|
});
|
|
276
|
-
it('should report no credentials found when service has no stored credentials', async () => {
|
|
277
|
-
const storePath = join(tempDir, 'credentials.json');
|
|
278
|
-
writeSecureFile(storePath, '{}');
|
|
279
|
-
const deps = createMockDependencies({
|
|
280
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
281
|
-
});
|
|
282
|
-
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
283
|
-
expect(logs.some((log) => log.includes('No API credentials found'))).toBe(true);
|
|
284
|
-
});
|
|
285
328
|
it('should return error for unknown service', async () => {
|
|
286
|
-
const
|
|
287
|
-
const deps = createMockDependencies({
|
|
288
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
289
|
-
});
|
|
329
|
+
const deps = createMockDependencies();
|
|
290
330
|
await runCommand(['auth', 'clear', 'unknown-service'], deps);
|
|
291
331
|
expect(exitCode).toBe(1);
|
|
292
|
-
expect(errorLogs.
|
|
293
|
-
});
|
|
294
|
-
it('should use default config paths', async () => {
|
|
295
|
-
const deps = createMockDependencies();
|
|
296
|
-
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
297
|
-
// With default paths, should report no credentials found (not error about missing env var)
|
|
298
|
-
expect(logs.some((log) => log.includes('No API credentials found'))).toBe(true);
|
|
332
|
+
expect(errorLogs.length).toBeGreaterThan(0);
|
|
299
333
|
});
|
|
300
334
|
it('should preserve other services when clearing one', async () => {
|
|
301
335
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -303,9 +337,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
303
337
|
slack: { objectType: 'slack', token: 'slack-token', dCookie: 'slack-cookie' },
|
|
304
338
|
discord: { objectType: 'authorizationBare', token: 'discord-token' },
|
|
305
339
|
}));
|
|
306
|
-
const deps = createMockDependencies(
|
|
307
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
308
|
-
});
|
|
340
|
+
const deps = createMockDependencies();
|
|
309
341
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
310
342
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
311
343
|
expect(storedData.slack).toBeUndefined();
|
|
@@ -317,23 +349,10 @@ describe('CLI commands with dependency injection', () => {
|
|
|
317
349
|
const browserStatePath = join(tempDir, 'browser_state.json');
|
|
318
350
|
writeSecureFile(storePath, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
|
|
319
351
|
writeSecureFile(browserStatePath, '{}');
|
|
320
|
-
const deps = createMockDependencies(
|
|
321
|
-
config: createMockConfig({ credentialStorePath: storePath, browserStatePath }),
|
|
322
|
-
});
|
|
352
|
+
const deps = createMockDependencies();
|
|
323
353
|
await runCommand(['auth', 'clear', '-y'], deps);
|
|
324
354
|
expect(existsSync(storePath)).toBe(false);
|
|
325
355
|
expect(existsSync(browserStatePath)).toBe(false);
|
|
326
|
-
expect(logs.some((log) => log.includes('Deleted credentials store'))).toBe(true);
|
|
327
|
-
expect(logs.some((log) => log.includes('Deleted browser state'))).toBe(true);
|
|
328
|
-
});
|
|
329
|
-
it('should report no files to delete when none exist', async () => {
|
|
330
|
-
const storePath = join(tempDir, 'nonexistent_store.json');
|
|
331
|
-
const browserStatePath = join(tempDir, 'nonexistent_browser_state.json');
|
|
332
|
-
const deps = createMockDependencies({
|
|
333
|
-
config: createMockConfig({ credentialStorePath: storePath, browserStatePath }),
|
|
334
|
-
});
|
|
335
|
-
await runCommand(['auth', 'clear', '-y'], deps);
|
|
336
|
-
expect(logs.some((log) => log.includes('No files to delete'))).toBe(true);
|
|
337
356
|
});
|
|
338
357
|
});
|
|
339
358
|
describe('auth list command', () => {
|
|
@@ -342,9 +361,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
342
361
|
writeSecureFile(storePath, JSON.stringify({
|
|
343
362
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
344
363
|
}));
|
|
345
|
-
const deps = createMockDependencies(
|
|
346
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
347
|
-
});
|
|
364
|
+
const deps = createMockDependencies();
|
|
348
365
|
await runCommand(['auth', 'list'], deps);
|
|
349
366
|
expect(logs).toHaveLength(1);
|
|
350
367
|
const entries = JSON.parse(logs[0] ?? '');
|
|
@@ -356,9 +373,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
356
373
|
it('should output empty object when no credentials are stored', async () => {
|
|
357
374
|
const storePath = join(tempDir, 'credentials.json');
|
|
358
375
|
writeSecureFile(storePath, '{}');
|
|
359
|
-
const deps = createMockDependencies(
|
|
360
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
361
|
-
});
|
|
376
|
+
const deps = createMockDependencies();
|
|
362
377
|
await runCommand(['auth', 'list'], deps);
|
|
363
378
|
expect(logs).toHaveLength(1);
|
|
364
379
|
const entries = JSON.parse(logs[0] ?? '');
|
|
@@ -369,9 +384,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
369
384
|
writeSecureFile(storePath, JSON.stringify({
|
|
370
385
|
unknown: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: secret'] },
|
|
371
386
|
}));
|
|
372
|
-
const deps = createMockDependencies(
|
|
373
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
374
|
-
});
|
|
387
|
+
const deps = createMockDependencies();
|
|
375
388
|
await runCommand(['auth', 'list'], deps);
|
|
376
389
|
expect(logs).toHaveLength(1);
|
|
377
390
|
const entries = JSON.parse(logs[0] ?? '');
|
|
@@ -385,9 +398,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
385
398
|
it('should store raw curl credentials', async () => {
|
|
386
399
|
const storePath = join(tempDir, 'credentials.json');
|
|
387
400
|
writeSecureFile(storePath, '{}');
|
|
388
|
-
const deps = createMockDependencies(
|
|
389
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
390
|
-
});
|
|
401
|
+
const deps = createMockDependencies();
|
|
391
402
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: secret', '-H', 'X-Other: value'], deps);
|
|
392
403
|
expect(logs).toContain('Credentials stored.');
|
|
393
404
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
@@ -400,30 +411,23 @@ describe('CLI commands with dependency injection', () => {
|
|
|
400
411
|
const deps = createMockDependencies();
|
|
401
412
|
await runCommand(['auth', 'set', 'slack'], deps);
|
|
402
413
|
expect(exitCode).toBe(1);
|
|
403
|
-
expect(errorLogs.some((log) => log.includes("don't look like valid curl options"))).toBe(true);
|
|
404
|
-
expect(errorLogs.some((log) => log.includes('Authorization: Bearer'))).toBe(true);
|
|
405
414
|
});
|
|
406
415
|
it('should return error when arguments lack curl switches', async () => {
|
|
407
416
|
const deps = createMockDependencies();
|
|
408
417
|
await runCommand(['auth', 'set', 'slack', 'my-raw-token-value'], deps);
|
|
409
418
|
expect(exitCode).toBe(1);
|
|
410
|
-
expect(errorLogs.some((log) => log.includes("don't look like valid curl options"))).toBe(true);
|
|
411
|
-
expect(errorLogs.some((log) => log.includes('Authorization: Bearer'))).toBe(true);
|
|
412
419
|
});
|
|
413
420
|
it('should return error for unknown service', async () => {
|
|
414
421
|
const deps = createMockDependencies();
|
|
415
422
|
await runCommand(['auth', 'set', 'unknown-service', '-H', 'X-Token: secret'], deps);
|
|
416
423
|
expect(exitCode).toBe(1);
|
|
417
|
-
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
418
424
|
});
|
|
419
425
|
it('should overwrite existing credentials', async () => {
|
|
420
426
|
const storePath = join(tempDir, 'credentials.json');
|
|
421
427
|
writeSecureFile(storePath, JSON.stringify({
|
|
422
428
|
slack: { objectType: 'slack', token: 'old-token', dCookie: 'old-cookie' },
|
|
423
429
|
}));
|
|
424
|
-
const deps = createMockDependencies(
|
|
425
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
426
|
-
});
|
|
430
|
+
const deps = createMockDependencies();
|
|
427
431
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: new-secret'], deps);
|
|
428
432
|
expect(logs).toContain('Credentials stored.');
|
|
429
433
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
@@ -433,15 +437,53 @@ describe('CLI commands with dependency injection', () => {
|
|
|
433
437
|
});
|
|
434
438
|
});
|
|
435
439
|
});
|
|
440
|
+
describe('auth set-nocurl command', () => {
|
|
441
|
+
it('should store telegram bot credentials', async () => {
|
|
442
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
443
|
+
writeSecureFile(storePath, '{}');
|
|
444
|
+
const deps = createMockDependencies({
|
|
445
|
+
registry: new Registry([TELEGRAM]),
|
|
446
|
+
});
|
|
447
|
+
await runCommand(['auth', 'set-nocurl', 'telegram', '123456:ABC-DEF'], deps);
|
|
448
|
+
expect(logs).toContain('Credentials stored.');
|
|
449
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
450
|
+
expect(storedData.telegram).toEqual({
|
|
451
|
+
objectType: 'telegramBot',
|
|
452
|
+
token: '123456:ABC-DEF',
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
it('should return error for unknown service', async () => {
|
|
456
|
+
const deps = createMockDependencies();
|
|
457
|
+
await runCommand(['auth', 'set-nocurl', 'unknown-service', 'some-arg'], deps);
|
|
458
|
+
expect(exitCode).toBe(1);
|
|
459
|
+
});
|
|
460
|
+
it('should return error when service does not support set-nocurl', async () => {
|
|
461
|
+
const deps = createMockDependencies();
|
|
462
|
+
await runCommand(['auth', 'set-nocurl', 'slack', 'some-token'], deps);
|
|
463
|
+
expect(exitCode).toBe(1);
|
|
464
|
+
});
|
|
465
|
+
it('should return error when telegram token is missing', async () => {
|
|
466
|
+
const deps = createMockDependencies({
|
|
467
|
+
registry: new Registry([TELEGRAM]),
|
|
468
|
+
});
|
|
469
|
+
await runCommand(['auth', 'set-nocurl', 'telegram'], deps);
|
|
470
|
+
expect(exitCode).toBe(1);
|
|
471
|
+
});
|
|
472
|
+
it('should return error when telegram token format is invalid', async () => {
|
|
473
|
+
const deps = createMockDependencies({
|
|
474
|
+
registry: new Registry([TELEGRAM]),
|
|
475
|
+
});
|
|
476
|
+
await runCommand(['auth', 'set-nocurl', 'telegram', 'not-a-valid-token'], deps);
|
|
477
|
+
expect(exitCode).toBe(1);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
436
480
|
describe('curl command', () => {
|
|
437
481
|
it('should pass arguments to subprocess', async () => {
|
|
438
482
|
const storePath = join(tempDir, 'credentials.json');
|
|
439
483
|
writeSecureFile(storePath, JSON.stringify({
|
|
440
484
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
441
485
|
}));
|
|
442
|
-
const deps = createMockDependencies(
|
|
443
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
444
|
-
});
|
|
486
|
+
const deps = createMockDependencies();
|
|
445
487
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
446
488
|
expect(capturedArgs).toEqual([
|
|
447
489
|
'-H',
|
|
@@ -457,9 +499,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
457
499
|
writeSecureFile(storePath, JSON.stringify({
|
|
458
500
|
slack: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Custom: header'] },
|
|
459
501
|
}));
|
|
460
|
-
const deps = createMockDependencies(
|
|
461
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
462
|
-
});
|
|
502
|
+
const deps = createMockDependencies();
|
|
463
503
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
464
504
|
expect(capturedArgs).toEqual(['-H', 'X-Custom: header', 'https://slack.com/api/test']);
|
|
465
505
|
expect(exitCode).toBe(0);
|
|
@@ -469,9 +509,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
469
509
|
writeSecureFile(storePath, JSON.stringify({
|
|
470
510
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
471
511
|
}));
|
|
472
|
-
const deps = createMockDependencies(
|
|
473
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
474
|
-
});
|
|
512
|
+
const deps = createMockDependencies();
|
|
475
513
|
await runCommand([
|
|
476
514
|
'curl',
|
|
477
515
|
'--',
|
|
@@ -494,7 +532,6 @@ describe('CLI commands with dependency injection', () => {
|
|
|
494
532
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
495
533
|
}));
|
|
496
534
|
const deps = createMockDependencies({
|
|
497
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
498
535
|
runCurl: () => ({ returncode: 42, stdout: '', stderr: '' }),
|
|
499
536
|
});
|
|
500
537
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
@@ -504,26 +541,11 @@ describe('CLI commands with dependency injection', () => {
|
|
|
504
541
|
const deps = createMockDependencies();
|
|
505
542
|
await runCommand(['curl', '--', '-X', 'POST'], deps);
|
|
506
543
|
expect(exitCode).toBe(1);
|
|
507
|
-
expect(errorLogs.some((log) => log.includes('Could not extract URL'))).toBe(true);
|
|
508
544
|
});
|
|
509
545
|
it('should return error for unknown service', async () => {
|
|
510
546
|
const deps = createMockDependencies();
|
|
511
547
|
await runCommand(['curl', 'https://unknown-api.example.com'], deps);
|
|
512
548
|
expect(exitCode).toBe(1);
|
|
513
|
-
expect(errorLogs.some((log) => log.includes('No service matches URL'))).toBe(true);
|
|
514
|
-
});
|
|
515
|
-
it('should inject credentials with verbose flag', async () => {
|
|
516
|
-
const storePath = join(tempDir, 'credentials.json');
|
|
517
|
-
writeSecureFile(storePath, JSON.stringify({
|
|
518
|
-
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
519
|
-
}));
|
|
520
|
-
const deps = createMockDependencies({
|
|
521
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
522
|
-
});
|
|
523
|
-
await runCommand(['curl', '--', '-v', 'https://slack.com/api/conversations.list'], deps);
|
|
524
|
-
expect(capturedArgs).toContain('-v');
|
|
525
|
-
expect(capturedArgs).toContain('Authorization: Bearer stored-token');
|
|
526
|
-
expect(capturedArgs).toContain('https://slack.com/api/conversations.list');
|
|
527
549
|
});
|
|
528
550
|
it('should read credentials from store and not call login', async () => {
|
|
529
551
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -539,11 +561,16 @@ describe('CLI commands with dependency injection', () => {
|
|
|
539
561
|
info: 'Test info for Slack service.',
|
|
540
562
|
credentialCheckCurlArguments: [],
|
|
541
563
|
checkApiCredentials: vi.fn(),
|
|
564
|
+
setCredentialsExample(serviceName) {
|
|
565
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
|
|
566
|
+
},
|
|
567
|
+
getCredentialsNoCurl() {
|
|
568
|
+
throw new NoCurlCredentialsNotSupportedError('slack');
|
|
569
|
+
},
|
|
542
570
|
getSession: vi.fn().mockReturnValue({ login: mockLogin }),
|
|
543
571
|
};
|
|
544
572
|
const deps = createMockDependencies({
|
|
545
573
|
registry: new Registry([mockSlackService]),
|
|
546
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
547
574
|
});
|
|
548
575
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
549
576
|
expect(mockLogin).not.toHaveBeenCalled();
|
|
@@ -552,14 +579,21 @@ describe('CLI commands with dependency injection', () => {
|
|
|
552
579
|
it('should return error when no credentials in store', async () => {
|
|
553
580
|
const storePath = join(tempDir, 'credentials.json');
|
|
554
581
|
writeSecureFile(storePath, '{}');
|
|
555
|
-
const deps = createMockDependencies(
|
|
556
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
557
|
-
});
|
|
582
|
+
const deps = createMockDependencies();
|
|
558
583
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
559
584
|
expect(exitCode).toBe(1);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
585
|
+
});
|
|
586
|
+
it('should inject telegram bot token into URL path', async () => {
|
|
587
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
588
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
589
|
+
telegram: { objectType: 'telegramBot', token: '123456:ABC-DEF' },
|
|
590
|
+
}));
|
|
591
|
+
const deps = createMockDependencies({
|
|
592
|
+
registry: new Registry([TELEGRAM]),
|
|
593
|
+
});
|
|
594
|
+
await runCommand(['curl', 'https://api.telegram.org/getMe'], deps);
|
|
595
|
+
expect(capturedArgs).toEqual(['https://api.telegram.org/bot123456:ABC-DEF/getMe']);
|
|
596
|
+
expect(exitCode).toBe(0);
|
|
563
597
|
});
|
|
564
598
|
it('should work when service does not have getSession but credentials exist', async () => {
|
|
565
599
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -574,11 +608,16 @@ describe('CLI commands with dependency injection', () => {
|
|
|
574
608
|
info: 'A service without browser login support.',
|
|
575
609
|
credentialCheckCurlArguments: [],
|
|
576
610
|
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
|
|
611
|
+
setCredentialsExample(serviceName) {
|
|
612
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
613
|
+
},
|
|
614
|
+
getCredentialsNoCurl() {
|
|
615
|
+
throw new NoCurlCredentialsNotSupportedError('nologin');
|
|
616
|
+
},
|
|
577
617
|
// No getSession - service doesn't support browser login
|
|
578
618
|
};
|
|
579
619
|
const deps = createMockDependencies({
|
|
580
620
|
registry: new Registry([noLoginService]),
|
|
581
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
582
621
|
});
|
|
583
622
|
await runCommand(['curl', 'https://nologin.example.com/api/test'], deps);
|
|
584
623
|
expect(exitCode).toBe(0);
|
|
@@ -596,6 +635,11 @@ describe('CLI commands with dependency injection', () => {
|
|
|
596
635
|
info: 'A service without browser login support.',
|
|
597
636
|
credentialCheckCurlArguments: [],
|
|
598
637
|
checkApiCredentials: vi.fn(),
|
|
638
|
+
setCredentialsExample(serviceName) {
|
|
639
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
640
|
+
},
|
|
641
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
642
|
+
getCredentialsNoCurl: Service.prototype.getCredentialsNoCurl,
|
|
599
643
|
// No getSession - service doesn't support browser login
|
|
600
644
|
};
|
|
601
645
|
const deps = createMockDependencies({
|
|
@@ -603,174 +647,630 @@ describe('CLI commands with dependency injection', () => {
|
|
|
603
647
|
});
|
|
604
648
|
await runCommand(['auth', 'browser', 'nologin'], deps);
|
|
605
649
|
expect(exitCode).toBe(1);
|
|
606
|
-
|
|
607
|
-
|
|
650
|
+
});
|
|
651
|
+
it('should suggest set-nocurl when service supports nocurl credentials', async () => {
|
|
652
|
+
const nocurlService = {
|
|
653
|
+
name: 'nocurl-only',
|
|
654
|
+
displayName: 'NoCurl Only Service',
|
|
655
|
+
baseApiUrls: ['https://nocurl.example.com/api/'],
|
|
656
|
+
loginUrl: 'https://nocurl.example.com',
|
|
657
|
+
info: 'A service with nocurl credentials but no browser login.',
|
|
658
|
+
credentialCheckCurlArguments: [],
|
|
659
|
+
checkApiCredentials: vi.fn(),
|
|
660
|
+
setCredentialsExample(serviceName) {
|
|
661
|
+
return `latchkey auth set-nocurl ${serviceName} <some-arg>`;
|
|
662
|
+
},
|
|
663
|
+
getCredentialsNoCurl(arguments_) {
|
|
664
|
+
if (arguments_.length !== 1) {
|
|
665
|
+
throw new Error('Expected exactly one argument');
|
|
666
|
+
}
|
|
667
|
+
return { objectType: 'test', injectIntoCurlCall: vi.fn(), isExpired: () => false };
|
|
668
|
+
},
|
|
669
|
+
// No getSession - service doesn't support browser login
|
|
670
|
+
};
|
|
671
|
+
const deps = createMockDependencies({
|
|
672
|
+
registry: new Registry([nocurlService]),
|
|
673
|
+
});
|
|
674
|
+
await runCommand(['auth', 'browser', 'nocurl-only'], deps);
|
|
675
|
+
expect(exitCode).toBe(1);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
describe('services register command', () => {
|
|
679
|
+
it('should register a new service', async () => {
|
|
680
|
+
const deps = createMockDependencies({
|
|
681
|
+
registry: new Registry([GITLAB]),
|
|
682
|
+
});
|
|
683
|
+
await runCommand([
|
|
684
|
+
'services',
|
|
685
|
+
'register',
|
|
686
|
+
'my-gitlab',
|
|
687
|
+
'--base-api-url',
|
|
688
|
+
'https://gitlab.mycompany.com/api/',
|
|
689
|
+
'--service-family',
|
|
690
|
+
'gitlab',
|
|
691
|
+
], deps);
|
|
692
|
+
expect(exitCode).toBeNull();
|
|
693
|
+
expect(logs).toContain("Service 'my-gitlab' registered.");
|
|
694
|
+
// Should be findable by name
|
|
695
|
+
expect(deps.registry.getByName('my-gitlab')).not.toBeNull();
|
|
696
|
+
// Should be findable by URL
|
|
697
|
+
expect(deps.registry.getByUrl('https://gitlab.mycompany.com/api/v4/user')).not.toBeNull();
|
|
698
|
+
});
|
|
699
|
+
it('should persist registration to config.json', async () => {
|
|
700
|
+
const deps = createMockDependencies({
|
|
701
|
+
registry: new Registry([GITLAB]),
|
|
702
|
+
});
|
|
703
|
+
await runCommand([
|
|
704
|
+
'services',
|
|
705
|
+
'register',
|
|
706
|
+
'my-gitlab',
|
|
707
|
+
'--base-api-url',
|
|
708
|
+
'https://gitlab.mycompany.com/api/',
|
|
709
|
+
'--service-family',
|
|
710
|
+
'gitlab',
|
|
711
|
+
], deps);
|
|
712
|
+
const configPath = deps.config.configPath;
|
|
713
|
+
const entries = loadRegisteredServices(configPath);
|
|
714
|
+
expect(entries.get('my-gitlab')).toEqual({
|
|
715
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
716
|
+
serviceFamily: 'gitlab',
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
it('should reject unknown service family', async () => {
|
|
720
|
+
const deps = createMockDependencies({
|
|
721
|
+
registry: new Registry([GITLAB]),
|
|
722
|
+
});
|
|
723
|
+
await runCommand([
|
|
724
|
+
'services',
|
|
725
|
+
'register',
|
|
726
|
+
'my-service',
|
|
727
|
+
'--base-api-url',
|
|
728
|
+
'https://example.com/api/',
|
|
729
|
+
'--service-family',
|
|
730
|
+
'nonexistent',
|
|
731
|
+
], deps);
|
|
732
|
+
expect(exitCode).toBe(1);
|
|
733
|
+
expect(errorLogs[0]).toContain('Unknown service family');
|
|
734
|
+
});
|
|
735
|
+
it('should reject duplicate service name', async () => {
|
|
736
|
+
const deps = createMockDependencies({
|
|
737
|
+
registry: new Registry([GITLAB]),
|
|
738
|
+
});
|
|
739
|
+
await runCommand([
|
|
740
|
+
'services',
|
|
741
|
+
'register',
|
|
742
|
+
'gitlab',
|
|
743
|
+
'--base-api-url',
|
|
744
|
+
'https://gitlab.mycompany.com/api/',
|
|
745
|
+
'--service-family',
|
|
746
|
+
'gitlab',
|
|
747
|
+
], deps);
|
|
748
|
+
expect(exitCode).toBe(1);
|
|
749
|
+
expect(errorLogs[0]).toContain('already exists');
|
|
750
|
+
});
|
|
751
|
+
it('should canonicalize service name to lowercase', async () => {
|
|
752
|
+
const deps = createMockDependencies({
|
|
753
|
+
registry: new Registry([GITLAB]),
|
|
754
|
+
});
|
|
755
|
+
await runCommand([
|
|
756
|
+
'services',
|
|
757
|
+
'register',
|
|
758
|
+
'My-GitLab',
|
|
759
|
+
'--base-api-url',
|
|
760
|
+
'https://gitlab.mycompany.com/api/',
|
|
761
|
+
'--service-family',
|
|
762
|
+
'gitlab',
|
|
763
|
+
], deps);
|
|
764
|
+
expect(exitCode).toBeNull();
|
|
765
|
+
expect(logs).toContain("Service 'my-gitlab' registered.");
|
|
766
|
+
expect(deps.registry.getByName('my-gitlab')).not.toBeNull();
|
|
767
|
+
});
|
|
768
|
+
it('should convert spaces to hyphens in service name', async () => {
|
|
769
|
+
const deps = createMockDependencies({
|
|
770
|
+
registry: new Registry([GITLAB]),
|
|
771
|
+
});
|
|
772
|
+
await runCommand(['services', 'register', 'my api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
773
|
+
expect(exitCode).toBeNull();
|
|
774
|
+
expect(logs).toContain("Service 'my-api' registered.");
|
|
775
|
+
expect(deps.registry.getByName('my-api')).not.toBeNull();
|
|
776
|
+
});
|
|
777
|
+
it('should reject service name with invalid characters', async () => {
|
|
778
|
+
const deps = createMockDependencies({
|
|
779
|
+
registry: new Registry([GITLAB]),
|
|
780
|
+
});
|
|
781
|
+
await runCommand(['services', 'register', 'my@service!', '--base-api-url', 'https://api.example.com/'], deps);
|
|
782
|
+
expect(exitCode).toBe(1);
|
|
783
|
+
expect(errorLogs[0]).toContain('Invalid service name');
|
|
784
|
+
});
|
|
785
|
+
it('should not expose browser auth without --login-url', async () => {
|
|
786
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
787
|
+
writeSecureFile(storePath, '{}');
|
|
788
|
+
const deps = createMockDependencies({
|
|
789
|
+
registry: new Registry([GITLAB]),
|
|
790
|
+
});
|
|
791
|
+
await runCommand([
|
|
792
|
+
'services',
|
|
793
|
+
'register',
|
|
794
|
+
'my-gitlab',
|
|
795
|
+
'--base-api-url',
|
|
796
|
+
'https://gitlab.mycompany.com/api/',
|
|
797
|
+
'--service-family',
|
|
798
|
+
'gitlab',
|
|
799
|
+
], deps);
|
|
800
|
+
logs = [];
|
|
801
|
+
exitCode = null;
|
|
802
|
+
await runCommand(['services', 'info', 'my-gitlab'], deps);
|
|
803
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
804
|
+
expect(info.authOptions).toEqual(['set']);
|
|
805
|
+
});
|
|
806
|
+
it('should persist and restore loginUrl', async () => {
|
|
807
|
+
const deps = createMockDependencies({
|
|
808
|
+
registry: new Registry([GITHUB]),
|
|
809
|
+
});
|
|
810
|
+
await runCommand([
|
|
811
|
+
'services',
|
|
812
|
+
'register',
|
|
813
|
+
'my-github',
|
|
814
|
+
'--base-api-url',
|
|
815
|
+
'https://github.mycompany.com/api/',
|
|
816
|
+
'--service-family',
|
|
817
|
+
'github',
|
|
818
|
+
'--login-url',
|
|
819
|
+
'https://github.mycompany.com/login',
|
|
820
|
+
], deps);
|
|
821
|
+
const entries = loadRegisteredServices(deps.config.configPath);
|
|
822
|
+
expect(entries.get('my-github')?.loginUrl).toBe('https://github.mycompany.com/login');
|
|
823
|
+
});
|
|
824
|
+
it('should reject --login-url without --service-family', async () => {
|
|
825
|
+
const deps = createMockDependencies({
|
|
826
|
+
registry: new Registry([]),
|
|
827
|
+
});
|
|
828
|
+
await runCommand([
|
|
829
|
+
'services',
|
|
830
|
+
'register',
|
|
831
|
+
'my-service',
|
|
832
|
+
'--base-api-url',
|
|
833
|
+
'https://example.com/api/',
|
|
834
|
+
'--login-url',
|
|
835
|
+
'https://example.com/login',
|
|
836
|
+
], deps);
|
|
837
|
+
expect(exitCode).toBe(1);
|
|
838
|
+
expect(errorLogs[0]).toContain('--login-url requires a --service-family');
|
|
839
|
+
});
|
|
840
|
+
it('should reject --login-url when service family does not support browser login', async () => {
|
|
841
|
+
const deps = createMockDependencies({
|
|
842
|
+
registry: new Registry([GITLAB]),
|
|
843
|
+
});
|
|
844
|
+
await runCommand([
|
|
845
|
+
'services',
|
|
846
|
+
'register',
|
|
847
|
+
'my-gitlab',
|
|
848
|
+
'--base-api-url',
|
|
849
|
+
'https://gitlab.mycompany.com/api/',
|
|
850
|
+
'--service-family',
|
|
851
|
+
'gitlab',
|
|
852
|
+
'--login-url',
|
|
853
|
+
'https://gitlab.mycompany.com/users/sign_in',
|
|
854
|
+
], deps);
|
|
855
|
+
expect(exitCode).toBe(1);
|
|
856
|
+
expect(errorLogs[0]).toContain('does not support browser login');
|
|
857
|
+
});
|
|
858
|
+
it('should require --login-url when service family supports browser login', async () => {
|
|
859
|
+
const deps = createMockDependencies({
|
|
860
|
+
registry: new Registry([GITHUB]),
|
|
861
|
+
});
|
|
862
|
+
await runCommand([
|
|
863
|
+
'services',
|
|
864
|
+
'register',
|
|
865
|
+
'my-github',
|
|
866
|
+
'--base-api-url',
|
|
867
|
+
'https://github.mycompany.com/api/',
|
|
868
|
+
'--service-family',
|
|
869
|
+
'github',
|
|
870
|
+
], deps);
|
|
871
|
+
expect(exitCode).toBe(1);
|
|
872
|
+
expect(errorLogs[0]).toContain('--login-url is required');
|
|
873
|
+
});
|
|
874
|
+
it('should make registered service usable with auth set', async () => {
|
|
875
|
+
const deps = createMockDependencies({
|
|
876
|
+
registry: new Registry([GITLAB]),
|
|
877
|
+
});
|
|
878
|
+
// Register the service
|
|
879
|
+
await runCommand([
|
|
880
|
+
'services',
|
|
881
|
+
'register',
|
|
882
|
+
'my-gitlab',
|
|
883
|
+
'--base-api-url',
|
|
884
|
+
'https://gitlab.mycompany.com/api/',
|
|
885
|
+
'--service-family',
|
|
886
|
+
'gitlab',
|
|
887
|
+
], deps);
|
|
888
|
+
// Now store credentials for it
|
|
889
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
890
|
+
writeSecureFile(storePath, '{}');
|
|
891
|
+
logs = [];
|
|
892
|
+
exitCode = null;
|
|
893
|
+
await runCommand(['auth', 'set', 'my-gitlab', '-H', 'PRIVATE-TOKEN: my-secret-token'], deps);
|
|
894
|
+
expect(exitCode).toBeNull();
|
|
895
|
+
expect(logs).toContain('Credentials stored.');
|
|
896
|
+
});
|
|
897
|
+
it('should register a service without --service-family', async () => {
|
|
898
|
+
const deps = createMockDependencies({
|
|
899
|
+
registry: new Registry([GITLAB]),
|
|
900
|
+
});
|
|
901
|
+
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
902
|
+
expect(exitCode).toBeNull();
|
|
903
|
+
expect(logs).toContain("Service 'my-api' registered.");
|
|
904
|
+
// Should be findable by name
|
|
905
|
+
expect(deps.registry.getByName('my-api')).not.toBeNull();
|
|
906
|
+
// Should be findable by URL
|
|
907
|
+
expect(deps.registry.getByUrl('https://api.example.com/v1/users')).not.toBeNull();
|
|
908
|
+
});
|
|
909
|
+
it('should persist registration without service family to config.json', async () => {
|
|
910
|
+
const deps = createMockDependencies({
|
|
911
|
+
registry: new Registry([GITLAB]),
|
|
912
|
+
});
|
|
913
|
+
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
914
|
+
const configPath = deps.config.configPath;
|
|
915
|
+
const entries = loadRegisteredServices(configPath);
|
|
916
|
+
expect(entries.get('my-api')).toEqual({
|
|
917
|
+
baseApiUrl: 'https://api.example.com/',
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
it('should not expose browser auth for service without family', async () => {
|
|
921
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
922
|
+
writeSecureFile(storePath, '{}');
|
|
923
|
+
const deps = createMockDependencies({
|
|
924
|
+
registry: new Registry([GITLAB]),
|
|
925
|
+
});
|
|
926
|
+
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
927
|
+
logs = [];
|
|
928
|
+
exitCode = null;
|
|
929
|
+
await runCommand(['services', 'info', 'my-api'], deps);
|
|
930
|
+
const info = JSON.parse(logs[0] ?? '');
|
|
931
|
+
expect(info.authOptions).toEqual(['set']);
|
|
932
|
+
});
|
|
933
|
+
it('should make service without family usable with auth set and curl', async () => {
|
|
934
|
+
const deps = createMockDependencies({
|
|
935
|
+
registry: new Registry([GITLAB]),
|
|
936
|
+
});
|
|
937
|
+
// Register the service without family
|
|
938
|
+
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
939
|
+
// Store credentials
|
|
940
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
941
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
942
|
+
'my-api': {
|
|
943
|
+
objectType: 'rawCurl',
|
|
944
|
+
curlArguments: ['-H', 'Authorization: Bearer my-token'],
|
|
945
|
+
},
|
|
946
|
+
}));
|
|
947
|
+
logs = [];
|
|
948
|
+
exitCode = null;
|
|
949
|
+
capturedArgs = [];
|
|
950
|
+
await runCommand(['curl', 'https://api.example.com/v1/users'], deps);
|
|
951
|
+
expect(exitCode).toBe(0);
|
|
952
|
+
expect(capturedArgs).toContain('-H');
|
|
953
|
+
expect(capturedArgs).toContain('Authorization: Bearer my-token');
|
|
954
|
+
});
|
|
955
|
+
it('should reject browser login for service without family', async () => {
|
|
956
|
+
const deps = createMockDependencies({
|
|
957
|
+
registry: new Registry([GITLAB]),
|
|
958
|
+
});
|
|
959
|
+
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
960
|
+
logs = [];
|
|
961
|
+
errorLogs = [];
|
|
962
|
+
exitCode = null;
|
|
963
|
+
await runCommand(['auth', 'browser', 'my-api'], deps);
|
|
964
|
+
expect(exitCode).toBe(1);
|
|
965
|
+
expect(errorLogs[0]).toContain('does not support browser flows');
|
|
966
|
+
});
|
|
967
|
+
it('should reject set-nocurl for service without family', async () => {
|
|
968
|
+
const deps = createMockDependencies({
|
|
969
|
+
registry: new Registry([GITLAB]),
|
|
970
|
+
});
|
|
971
|
+
await runCommand(['services', 'register', 'my-api', '--base-api-url', 'https://api.example.com/'], deps);
|
|
972
|
+
logs = [];
|
|
973
|
+
errorLogs = [];
|
|
974
|
+
exitCode = null;
|
|
975
|
+
await runCommand(['auth', 'set-nocurl', 'my-api', 'some-token'], deps);
|
|
976
|
+
expect(exitCode).toBe(1);
|
|
977
|
+
expect(errorLogs[0]).toContain('does not support set-nocurl');
|
|
978
|
+
});
|
|
979
|
+
it('should make registered service usable with curl', async () => {
|
|
980
|
+
const deps = createMockDependencies({
|
|
981
|
+
registry: new Registry([GITLAB]),
|
|
982
|
+
});
|
|
983
|
+
// Register the service
|
|
984
|
+
await runCommand([
|
|
985
|
+
'services',
|
|
986
|
+
'register',
|
|
987
|
+
'my-gitlab',
|
|
988
|
+
'--base-api-url',
|
|
989
|
+
'https://gitlab.mycompany.com/api/',
|
|
990
|
+
'--service-family',
|
|
991
|
+
'gitlab',
|
|
992
|
+
], deps);
|
|
993
|
+
// Store credentials
|
|
994
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
995
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
996
|
+
'my-gitlab': {
|
|
997
|
+
objectType: 'rawCurl',
|
|
998
|
+
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
999
|
+
},
|
|
1000
|
+
}));
|
|
1001
|
+
logs = [];
|
|
1002
|
+
exitCode = null;
|
|
1003
|
+
capturedArgs = [];
|
|
1004
|
+
await runCommand(['curl', 'https://gitlab.mycompany.com/api/v4/user'], deps);
|
|
1005
|
+
expect(exitCode).toBe(0);
|
|
1006
|
+
expect(capturedArgs).toContain('-H');
|
|
1007
|
+
expect(capturedArgs).toContain('PRIVATE-TOKEN: my-secret-token');
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
describe('services deregister command', () => {
|
|
1011
|
+
it('should deregister a registered service', async () => {
|
|
1012
|
+
const deps = createMockDependencies({
|
|
1013
|
+
registry: new Registry([GITLAB]),
|
|
1014
|
+
});
|
|
1015
|
+
// Register a service first
|
|
1016
|
+
await runCommand([
|
|
1017
|
+
'services',
|
|
1018
|
+
'register',
|
|
1019
|
+
'my-gitlab',
|
|
1020
|
+
'--base-api-url',
|
|
1021
|
+
'https://gitlab.mycompany.com/api/',
|
|
1022
|
+
'--service-family',
|
|
1023
|
+
'gitlab',
|
|
1024
|
+
], deps);
|
|
1025
|
+
logs = [];
|
|
1026
|
+
exitCode = null;
|
|
1027
|
+
await runCommand(['services', 'deregister', 'my-gitlab'], deps);
|
|
1028
|
+
expect(exitCode).toBeNull();
|
|
1029
|
+
expect(logs).toContain("Service 'my-gitlab' deregistered.");
|
|
1030
|
+
});
|
|
1031
|
+
it('should remove service from config.json', async () => {
|
|
1032
|
+
const deps = createMockDependencies({
|
|
1033
|
+
registry: new Registry([GITLAB]),
|
|
1034
|
+
});
|
|
1035
|
+
await runCommand([
|
|
1036
|
+
'services',
|
|
1037
|
+
'register',
|
|
1038
|
+
'my-gitlab',
|
|
1039
|
+
'--base-api-url',
|
|
1040
|
+
'https://gitlab.mycompany.com/api/',
|
|
1041
|
+
'--service-family',
|
|
1042
|
+
'gitlab',
|
|
1043
|
+
], deps);
|
|
1044
|
+
logs = [];
|
|
1045
|
+
exitCode = null;
|
|
1046
|
+
await runCommand(['services', 'deregister', 'my-gitlab'], deps);
|
|
1047
|
+
const entries = loadRegisteredServices(deps.config.configPath);
|
|
1048
|
+
expect(entries.get('my-gitlab')).toBeUndefined();
|
|
1049
|
+
});
|
|
1050
|
+
it('should reject deregistering an unknown service', async () => {
|
|
1051
|
+
const deps = createMockDependencies();
|
|
1052
|
+
await runCommand(['services', 'deregister', 'nonexistent'], deps);
|
|
1053
|
+
expect(exitCode).toBe(1);
|
|
1054
|
+
expect(errorLogs[0]).toContain('Unknown service');
|
|
1055
|
+
});
|
|
1056
|
+
it('should reject deregistering a built-in service', async () => {
|
|
1057
|
+
const deps = createMockDependencies();
|
|
1058
|
+
await runCommand(['services', 'deregister', 'slack'], deps);
|
|
1059
|
+
expect(exitCode).toBe(1);
|
|
1060
|
+
expect(errorLogs[0]).toContain('built-in service');
|
|
1061
|
+
});
|
|
1062
|
+
it('should reject deregistering when credentials still exist', async () => {
|
|
1063
|
+
const deps = createMockDependencies({
|
|
1064
|
+
registry: new Registry([GITLAB]),
|
|
1065
|
+
});
|
|
1066
|
+
// Register a service
|
|
1067
|
+
await runCommand([
|
|
1068
|
+
'services',
|
|
1069
|
+
'register',
|
|
1070
|
+
'my-gitlab',
|
|
1071
|
+
'--base-api-url',
|
|
1072
|
+
'https://gitlab.mycompany.com/api/',
|
|
1073
|
+
'--service-family',
|
|
1074
|
+
'gitlab',
|
|
1075
|
+
], deps);
|
|
1076
|
+
// Store credentials for it
|
|
1077
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
1078
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1079
|
+
'my-gitlab': {
|
|
1080
|
+
objectType: 'rawCurl',
|
|
1081
|
+
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
1082
|
+
},
|
|
1083
|
+
}));
|
|
1084
|
+
logs = [];
|
|
1085
|
+
errorLogs = [];
|
|
1086
|
+
exitCode = null;
|
|
1087
|
+
await runCommand(['services', 'deregister', 'my-gitlab'], deps);
|
|
1088
|
+
expect(exitCode).toBe(1);
|
|
1089
|
+
expect(errorLogs[0]).toContain('Credentials still exist');
|
|
1090
|
+
expect(errorLogs[0]).toContain('latchkey auth clear my-gitlab');
|
|
1091
|
+
// Service should still be in config
|
|
1092
|
+
const entries = loadRegisteredServices(deps.config.configPath);
|
|
1093
|
+
expect(entries.get('my-gitlab')).toBeDefined();
|
|
1094
|
+
});
|
|
1095
|
+
it('should allow deregistering after credentials are cleared', async () => {
|
|
1096
|
+
const deps = createMockDependencies({
|
|
1097
|
+
registry: new Registry([GITLAB]),
|
|
1098
|
+
});
|
|
1099
|
+
// Register
|
|
1100
|
+
await runCommand([
|
|
1101
|
+
'services',
|
|
1102
|
+
'register',
|
|
1103
|
+
'my-gitlab',
|
|
1104
|
+
'--base-api-url',
|
|
1105
|
+
'https://gitlab.mycompany.com/api/',
|
|
1106
|
+
'--service-family',
|
|
1107
|
+
'gitlab',
|
|
1108
|
+
], deps);
|
|
1109
|
+
// Store and then clear credentials
|
|
1110
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
1111
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
1112
|
+
'my-gitlab': {
|
|
1113
|
+
objectType: 'rawCurl',
|
|
1114
|
+
curlArguments: ['-H', 'PRIVATE-TOKEN: my-secret-token'],
|
|
1115
|
+
},
|
|
1116
|
+
}));
|
|
1117
|
+
logs = [];
|
|
1118
|
+
errorLogs = [];
|
|
1119
|
+
exitCode = null;
|
|
1120
|
+
await runCommand(['auth', 'clear', 'my-gitlab'], deps);
|
|
1121
|
+
expect(exitCode).toBeNull();
|
|
1122
|
+
logs = [];
|
|
1123
|
+
errorLogs = [];
|
|
1124
|
+
exitCode = null;
|
|
1125
|
+
await runCommand(['services', 'deregister', 'my-gitlab'], deps);
|
|
1126
|
+
expect(exitCode).toBeNull();
|
|
1127
|
+
expect(logs).toContain("Service 'my-gitlab' deregistered.");
|
|
608
1128
|
});
|
|
609
1129
|
});
|
|
610
1130
|
});
|
|
611
|
-
|
|
612
|
-
describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
1131
|
+
describe('registeredServiceStore', () => {
|
|
613
1132
|
let tempDir;
|
|
614
|
-
let testEnv;
|
|
615
1133
|
beforeEach(() => {
|
|
616
|
-
tempDir = mkdtempSync(join(tmpdir(), 'latchkey-
|
|
617
|
-
testEnv = {
|
|
618
|
-
LATCHKEY_STORE: join(tempDir, 'credentials.json'),
|
|
619
|
-
LATCHKEY_BROWSER_STATE: join(tempDir, 'browser_state.json'),
|
|
620
|
-
};
|
|
1134
|
+
tempDir = mkdtempSync(join(tmpdir(), 'latchkey-store-test-'));
|
|
621
1135
|
});
|
|
622
1136
|
afterEach(() => {
|
|
623
1137
|
rmSync(tempDir, { recursive: true, force: true });
|
|
624
1138
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
});
|
|
631
|
-
it('should return error when no URL found in curl arguments', () => {
|
|
632
|
-
const result = runCli(['curl', '--', '-X', 'POST'], testEnv);
|
|
633
|
-
expect(result.exitCode).toBe(1);
|
|
634
|
-
expect(result.stderr).toContain('Could not extract URL');
|
|
635
|
-
});
|
|
636
|
-
it('should return error for unknown service', () => {
|
|
637
|
-
const result = runCli(['curl', 'https://unknown-api.example.com'], testEnv);
|
|
638
|
-
expect(result.exitCode).toBe(1);
|
|
639
|
-
expect(result.stderr).toContain('No service matches URL');
|
|
640
|
-
expect(result.stderr).toContain('https://unknown-api.example.com');
|
|
641
|
-
});
|
|
642
|
-
it('should return error when no credentials exist', () => {
|
|
643
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
644
|
-
const result = runCli(['curl', 'https://slack.com/api/test'], testEnv);
|
|
645
|
-
expect(result.exitCode).toBe(1);
|
|
646
|
-
expect(result.stderr).toContain('No credentials found for slack');
|
|
647
|
-
expect(result.stderr).toContain('auth browser');
|
|
648
|
-
expect(result.stderr).toContain('auth set');
|
|
1139
|
+
it('should save and load registered services', () => {
|
|
1140
|
+
const configPath = join(tempDir, 'config.json');
|
|
1141
|
+
saveRegisteredService(configPath, 'my-gitlab', {
|
|
1142
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1143
|
+
serviceFamily: 'gitlab',
|
|
649
1144
|
});
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
LATCHKEY_DISABLE_BROWSER: '1',
|
|
656
|
-
});
|
|
657
|
-
expect(result.exitCode).toBe(1);
|
|
658
|
-
expect(result.stderr).toContain('Browser is disabled');
|
|
1145
|
+
const entries = loadRegisteredServices(configPath);
|
|
1146
|
+
expect(entries.size).toBe(1);
|
|
1147
|
+
expect(entries.get('my-gitlab')).toEqual({
|
|
1148
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1149
|
+
serviceFamily: 'gitlab',
|
|
659
1150
|
});
|
|
660
1151
|
});
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
it('should report no credentials found when service has no stored credentials', () => {
|
|
673
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
674
|
-
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
675
|
-
expect(result.exitCode).toBe(0);
|
|
676
|
-
expect(result.stdout).toContain('No API credentials found for slack');
|
|
677
|
-
});
|
|
678
|
-
it('should return error for unknown service', () => {
|
|
679
|
-
const result = runCli(['auth', 'clear', 'unknown-service'], testEnv);
|
|
680
|
-
expect(result.exitCode).toBe(1);
|
|
681
|
-
expect(result.stderr).toContain('Unknown service: unknown-service');
|
|
1152
|
+
it('should return empty map for nonexistent config file', () => {
|
|
1153
|
+
const configPath = join(tempDir, 'nonexistent.json');
|
|
1154
|
+
const entries = loadRegisteredServices(configPath);
|
|
1155
|
+
expect(entries.size).toBe(0);
|
|
1156
|
+
});
|
|
1157
|
+
it('should preserve existing config data when saving', () => {
|
|
1158
|
+
const configPath = join(tempDir, 'config.json');
|
|
1159
|
+
writeFileSync(configPath, JSON.stringify({ browser: { executablePath: '/usr/bin/chrome' } }));
|
|
1160
|
+
saveRegisteredService(configPath, 'my-gitlab', {
|
|
1161
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1162
|
+
serviceFamily: 'gitlab',
|
|
682
1163
|
});
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
expect(storedData.discord).toBeDefined();
|
|
693
|
-
expect(storedData.discord?.token).toBe('discord-token');
|
|
1164
|
+
const content = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
1165
|
+
expect(content.browser).toEqual({ executablePath: '/usr/bin/chrome' });
|
|
1166
|
+
expect(content.registeredServices).toBeDefined();
|
|
1167
|
+
});
|
|
1168
|
+
it('should load registered services into registry', () => {
|
|
1169
|
+
const configPath = join(tempDir, 'config.json');
|
|
1170
|
+
saveRegisteredService(configPath, 'my-gitlab', {
|
|
1171
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1172
|
+
serviceFamily: 'gitlab',
|
|
694
1173
|
});
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
// browser_state does not exist
|
|
708
|
-
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
709
|
-
expect(result.exitCode).toBe(0);
|
|
710
|
-
expect(existsSync(testEnv.LATCHKEY_STORE)).toBe(false);
|
|
711
|
-
expect(result.stdout).toContain(`Deleted credentials store: ${testEnv.LATCHKEY_STORE}`);
|
|
712
|
-
expect(result.stdout).not.toContain('browser state');
|
|
713
|
-
});
|
|
714
|
-
it('should report no files to delete when none exist', () => {
|
|
715
|
-
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
716
|
-
expect(result.exitCode).toBe(0);
|
|
717
|
-
expect(result.stdout).toContain('No files to delete');
|
|
1174
|
+
const registry = new Registry([GITLAB]);
|
|
1175
|
+
loadRegisteredServicesIntoRegistry(configPath, registry);
|
|
1176
|
+
const service = registry.getByName('my-gitlab');
|
|
1177
|
+
expect(service).not.toBeNull();
|
|
1178
|
+
expect(service.baseApiUrls).toEqual(['https://gitlab.mycompany.com/api/']);
|
|
1179
|
+
});
|
|
1180
|
+
it('should load registered service with loginUrl into registry', () => {
|
|
1181
|
+
const configPath = join(tempDir, 'config.json');
|
|
1182
|
+
saveRegisteredService(configPath, 'my-gitlab', {
|
|
1183
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1184
|
+
serviceFamily: 'gitlab',
|
|
1185
|
+
loginUrl: 'https://gitlab.mycompany.com/users/sign_in',
|
|
718
1186
|
});
|
|
1187
|
+
const registry = new Registry([GITLAB]);
|
|
1188
|
+
loadRegisteredServicesIntoRegistry(configPath, registry);
|
|
1189
|
+
const service = registry.getByName('my-gitlab');
|
|
1190
|
+
expect(service).not.toBeNull();
|
|
1191
|
+
expect(service.loginUrl).toBe('https://gitlab.mycompany.com/users/sign_in');
|
|
719
1192
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}));
|
|
725
|
-
const result = runCli(['auth', 'list'], testEnv);
|
|
726
|
-
expect(result.exitCode).toBe(0);
|
|
727
|
-
const entries = JSON.parse(result.stdout);
|
|
728
|
-
expect(entries.slack).toBeDefined();
|
|
729
|
-
expect(entries.slack?.credentialType).toBe('slack');
|
|
730
|
-
expect(entries.slack?.credentialStatus).toEqual(expect.any(String));
|
|
731
|
-
});
|
|
732
|
-
it('should output empty object when no credentials are stored', () => {
|
|
733
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
734
|
-
const result = runCli(['auth', 'list'], testEnv);
|
|
735
|
-
expect(result.exitCode).toBe(0);
|
|
736
|
-
const entries = JSON.parse(result.stdout);
|
|
737
|
-
expect(Object.keys(entries)).toHaveLength(0);
|
|
1193
|
+
it('should load registered service without family into registry', () => {
|
|
1194
|
+
const configPath = join(tempDir, 'config.json');
|
|
1195
|
+
saveRegisteredService(configPath, 'my-api', {
|
|
1196
|
+
baseApiUrl: 'https://api.example.com/',
|
|
738
1197
|
});
|
|
1198
|
+
const registry = new Registry([GITLAB]);
|
|
1199
|
+
loadRegisteredServicesIntoRegistry(configPath, registry);
|
|
1200
|
+
const service = registry.getByName('my-api');
|
|
1201
|
+
expect(service).not.toBeNull();
|
|
1202
|
+
expect(service.baseApiUrls).toEqual(['https://api.example.com/']);
|
|
1203
|
+
expect(service.getSession).toBeUndefined(); // eslint-disable-line @typescript-eslint/unbound-method
|
|
1204
|
+
expect(service.loginUrl).toBe('');
|
|
739
1205
|
});
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
expect(services).toContain('slack');
|
|
746
|
-
expect(services).toContain('discord');
|
|
747
|
-
expect(services).toContain('github');
|
|
748
|
-
expect(services).toContain('dropbox');
|
|
749
|
-
expect(services).toContain('linear');
|
|
1206
|
+
it('should skip registered services with unknown family', () => {
|
|
1207
|
+
const configPath = join(tempDir, 'config.json');
|
|
1208
|
+
saveRegisteredService(configPath, 'my-unknown', {
|
|
1209
|
+
baseApiUrl: 'https://unknown.example.com/api/',
|
|
1210
|
+
serviceFamily: 'nonexistent',
|
|
750
1211
|
});
|
|
1212
|
+
const registry = new Registry([GITLAB]);
|
|
1213
|
+
loadRegisteredServicesIntoRegistry(configPath, registry);
|
|
1214
|
+
expect(registry.getByName('my-unknown')).toBeNull();
|
|
751
1215
|
});
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const info = JSON.parse(result.stdout);
|
|
758
|
-
expect(info.authOptions).toEqual(['browser', 'set']);
|
|
759
|
-
expect(info.credentialStatus).toBe('missing');
|
|
760
|
-
expect(info.developerNotes).toEqual(expect.any(String));
|
|
1216
|
+
it('should delete a registered service from config', () => {
|
|
1217
|
+
const configPath = join(tempDir, 'config.json');
|
|
1218
|
+
saveRegisteredService(configPath, 'my-gitlab', {
|
|
1219
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1220
|
+
serviceFamily: 'gitlab',
|
|
761
1221
|
});
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1222
|
+
saveRegisteredService(configPath, 'my-github', {
|
|
1223
|
+
baseApiUrl: 'https://github.mycompany.com/api/',
|
|
1224
|
+
serviceFamily: 'github',
|
|
1225
|
+
});
|
|
1226
|
+
deleteRegisteredService(configPath, 'my-gitlab');
|
|
1227
|
+
const entries = loadRegisteredServices(configPath);
|
|
1228
|
+
expect(entries.get('my-gitlab')).toBeUndefined();
|
|
1229
|
+
expect(entries.get('my-github')).toBeDefined();
|
|
1230
|
+
});
|
|
1231
|
+
it('should preserve other config data when deleting', () => {
|
|
1232
|
+
const configPath = join(tempDir, 'config.json');
|
|
1233
|
+
writeFileSync(configPath, JSON.stringify({ browser: { executablePath: '/usr/bin/chrome' } }));
|
|
1234
|
+
saveRegisteredService(configPath, 'my-gitlab', {
|
|
1235
|
+
baseApiUrl: 'https://gitlab.mycompany.com/api/',
|
|
1236
|
+
serviceFamily: 'gitlab',
|
|
768
1237
|
});
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1238
|
+
deleteRegisteredService(configPath, 'my-gitlab');
|
|
1239
|
+
const content = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
1240
|
+
expect(content.browser).toEqual({ executablePath: '/usr/bin/chrome' });
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
// Integration tests that run the actual CLI binary.
|
|
1244
|
+
// Only tests that exercise behavior not covered by the DI unit tests above.
|
|
1245
|
+
describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
1246
|
+
let tempDir;
|
|
1247
|
+
let testEnv;
|
|
1248
|
+
beforeEach(() => {
|
|
1249
|
+
tempDir = mkdtempSync(join(tmpdir(), 'latchkey-cli-test-'));
|
|
1250
|
+
testEnv = {
|
|
1251
|
+
LATCHKEY_DIRECTORY: tempDir,
|
|
1252
|
+
};
|
|
1253
|
+
});
|
|
1254
|
+
afterEach(() => {
|
|
1255
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
1256
|
+
});
|
|
1257
|
+
it('should return non-zero exit code for unknown service URL', () => {
|
|
1258
|
+
const result = runCli(['curl', 'https://unknown-api.example.com'], testEnv);
|
|
1259
|
+
expect(result.exitCode).toBe(1);
|
|
1260
|
+
});
|
|
1261
|
+
it('should return error when browser is disabled via LATCHKEY_DISABLE_BROWSER', () => {
|
|
1262
|
+
const result = runCli(['auth', 'browser', 'slack'], {
|
|
1263
|
+
...testEnv,
|
|
1264
|
+
LATCHKEY_DISABLE_BROWSER: '1',
|
|
773
1265
|
});
|
|
1266
|
+
expect(result.exitCode).toBe(1);
|
|
1267
|
+
});
|
|
1268
|
+
it('should list services as JSON', () => {
|
|
1269
|
+
const result = runCli(['services', 'list'], testEnv);
|
|
1270
|
+
expect(result.exitCode).toBe(0);
|
|
1271
|
+
const services = JSON.parse(result.stdout.trim());
|
|
1272
|
+
expect(services).toContain('slack');
|
|
1273
|
+
expect(services).toContain('github');
|
|
774
1274
|
});
|
|
775
1275
|
});
|
|
776
1276
|
//# sourceMappingURL=cli.test.js.map
|