latchkey 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -7
- package/dist/integrations/SKILL.md +9 -2
- package/dist/package.json +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 +11 -114
- package/dist/src/apiCredentials.d.ts.map +1 -1
- package/dist/src/apiCredentials.js +9 -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 +1 -5
- package/dist/src/browserConfig.d.ts.map +1 -1
- package/dist/src/browserConfig.js +2 -8
- package/dist/src/browserConfig.js.map +1 -1
- package/dist/src/cli.js +20 -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 +50 -40
- 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/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.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/registry.d.ts.map +1 -1
- package/dist/src/registry.js +18 -2
- 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/discord.d.ts +1 -0
- package/dist/src/services/discord.d.ts.map +1 -1
- package/dist/src/services/discord.js +3 -0
- package/dist/src/services/discord.js.map +1 -1
- package/dist/src/services/dropbox.d.ts +2 -1
- package/dist/src/services/dropbox.d.ts.map +1 -1
- package/dist/src/services/dropbox.js +5 -0
- 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 +1 -0
- package/dist/src/services/github.d.ts.map +1 -1
- package/dist/src/services/github.js +3 -0
- 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 +19 -2
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +19 -2
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/linear.d.ts +1 -0
- package/dist/src/services/linear.d.ts.map +1 -1
- package/dist/src/services/linear.js +3 -0
- package/dist/src/services/linear.js.map +1 -1
- package/dist/src/services/mailchimp.d.ts +2 -1
- package/dist/src/services/mailchimp.d.ts.map +1 -1
- package/dist/src/services/mailchimp.js +4 -3
- package/dist/src/services/mailchimp.js.map +1 -1
- package/dist/src/services/notion.d.ts +1 -0
- package/dist/src/services/notion.d.ts.map +1 -1
- package/dist/src/services/notion.js +3 -0
- 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 +30 -1
- package/dist/src/services/slack.d.ts.map +1 -1
- package/dist/src/services/slack.js +45 -2
- 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 +160 -260
- 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 +49 -75
- package/dist/tests/registry.test.js.map +1 -1
- package/dist/tests/servicesAgainstRecordings.test.js +1 -1
- package/dist/tests/servicesAgainstRecordings.test.js.map +1 -1
- package/package.json +1 -1
package/dist/tests/cli.test.js
CHANGED
|
@@ -4,12 +4,15 @@ 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/base.js';
|
|
15
|
+
import { TELEGRAM } from '../src/services/telegram.js';
|
|
13
16
|
// Use a fixed test key for deterministic test behavior (32 bytes = 256 bits, base64 encoded)
|
|
14
17
|
const TEST_ENCRYPTION_KEY = 'dGVzdGtleXRlc3RrZXl0ZXN0a2V5dGVzdGtleXRlc3Q=';
|
|
15
18
|
function writeSecureFile(path, content) {
|
|
@@ -115,10 +118,18 @@ describe('CLI commands with dependency injection', () => {
|
|
|
115
118
|
let exitCode;
|
|
116
119
|
function createMockConfig(overrides = {}) {
|
|
117
120
|
const defaultConfig = new Config(() => undefined);
|
|
121
|
+
const directory = overrides.directory ?? tempDir;
|
|
118
122
|
return {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
directory,
|
|
124
|
+
get credentialStorePath() {
|
|
125
|
+
return join(directory, 'credentials.json');
|
|
126
|
+
},
|
|
127
|
+
get browserStatePath() {
|
|
128
|
+
return join(directory, 'browser_state.json');
|
|
129
|
+
},
|
|
130
|
+
get configPath() {
|
|
131
|
+
return join(directory, 'config.json');
|
|
132
|
+
},
|
|
122
133
|
curlCommand: overrides.curlCommand ?? defaultConfig.curlCommand,
|
|
123
134
|
encryptionKeyOverride: overrides.encryptionKeyOverride ?? TEST_ENCRYPTION_KEY,
|
|
124
135
|
serviceName: overrides.serviceName ?? defaultConfig.serviceName,
|
|
@@ -137,6 +148,12 @@ describe('CLI commands with dependency injection', () => {
|
|
|
137
148
|
info: 'Test info for Slack service.',
|
|
138
149
|
credentialCheckCurlArguments: ['https://slack.com/api/auth.test'],
|
|
139
150
|
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
|
|
151
|
+
setCredentialsExample(serviceName) {
|
|
152
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
|
|
153
|
+
},
|
|
154
|
+
getCredentialsNoCurl() {
|
|
155
|
+
throw new NoCurlCredentialsNotSupportedError('slack');
|
|
156
|
+
},
|
|
140
157
|
getSession: vi.fn().mockReturnValue({
|
|
141
158
|
login: vi.fn().mockResolvedValue(new SlackApiCredentials('xoxc-test-token', 'test-cookie')),
|
|
142
159
|
}),
|
|
@@ -200,14 +217,13 @@ describe('CLI commands with dependency injection', () => {
|
|
|
200
217
|
it('should show login options, credentials status, and developer notes', async () => {
|
|
201
218
|
const storePath = join(tempDir, 'credentials.json');
|
|
202
219
|
writeSecureFile(storePath, '{}');
|
|
203
|
-
const deps = createMockDependencies(
|
|
204
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
205
|
-
});
|
|
220
|
+
const deps = createMockDependencies();
|
|
206
221
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
207
222
|
expect(logs).toHaveLength(1);
|
|
208
223
|
const info = JSON.parse(logs[0] ?? '');
|
|
209
224
|
expect(info.authOptions).toEqual(['browser', 'set']);
|
|
210
225
|
expect(info.credentialStatus).toBe('missing');
|
|
226
|
+
expect(info.setCredentialsExample).toBe('latchkey auth set slack -H "Authorization: Bearer xoxb-your-token"');
|
|
211
227
|
expect(info.developerNotes).toBe('Test info for Slack service.');
|
|
212
228
|
});
|
|
213
229
|
it('should show auth set only for services without browser login', async () => {
|
|
@@ -221,10 +237,15 @@ describe('CLI commands with dependency injection', () => {
|
|
|
221
237
|
info: 'A service without browser login support.',
|
|
222
238
|
credentialCheckCurlArguments: [],
|
|
223
239
|
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Missing),
|
|
240
|
+
setCredentialsExample(serviceName) {
|
|
241
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
242
|
+
},
|
|
243
|
+
getCredentialsNoCurl() {
|
|
244
|
+
throw new NoCurlCredentialsNotSupportedError('nologin');
|
|
245
|
+
},
|
|
224
246
|
};
|
|
225
247
|
const deps = createMockDependencies({
|
|
226
248
|
registry: new Registry([noLoginService]),
|
|
227
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
228
249
|
});
|
|
229
250
|
await runCommand(['services', 'info', 'nologin'], deps);
|
|
230
251
|
const info = JSON.parse(logs[0] ?? '');
|
|
@@ -234,7 +255,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
234
255
|
const storePath = join(tempDir, 'credentials.json');
|
|
235
256
|
writeSecureFile(storePath, '{}');
|
|
236
257
|
const deps = createMockDependencies({
|
|
237
|
-
config: createMockConfig({
|
|
258
|
+
config: createMockConfig({ browserDisabled: true }),
|
|
238
259
|
});
|
|
239
260
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
240
261
|
const info = JSON.parse(logs[0] ?? '');
|
|
@@ -245,9 +266,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
245
266
|
writeSecureFile(storePath, JSON.stringify({
|
|
246
267
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
247
268
|
}));
|
|
248
|
-
const deps = createMockDependencies(
|
|
249
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
250
|
-
});
|
|
269
|
+
const deps = createMockDependencies();
|
|
251
270
|
await runCommand(['services', 'info', 'slack'], deps);
|
|
252
271
|
const info = JSON.parse(logs[0] ?? '');
|
|
253
272
|
expect(info.credentialStatus).toBe('valid');
|
|
@@ -256,7 +275,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
256
275
|
const deps = createMockDependencies();
|
|
257
276
|
await runCommand(['services', 'info', 'unknown-service'], deps);
|
|
258
277
|
expect(exitCode).toBe(1);
|
|
259
|
-
expect(errorLogs.
|
|
278
|
+
expect(errorLogs.length).toBeGreaterThan(0);
|
|
260
279
|
});
|
|
261
280
|
});
|
|
262
281
|
describe('clear command', () => {
|
|
@@ -265,37 +284,16 @@ describe('CLI commands with dependency injection', () => {
|
|
|
265
284
|
writeSecureFile(storePath, JSON.stringify({
|
|
266
285
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
267
286
|
}));
|
|
268
|
-
const deps = createMockDependencies(
|
|
269
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
270
|
-
});
|
|
287
|
+
const deps = createMockDependencies();
|
|
271
288
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
272
|
-
expect(logs.some((log) => log.includes('have been cleared'))).toBe(true);
|
|
273
289
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
274
290
|
expect(storedData.slack).toBeUndefined();
|
|
275
291
|
});
|
|
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
292
|
it('should return error for unknown service', async () => {
|
|
286
|
-
const
|
|
287
|
-
const deps = createMockDependencies({
|
|
288
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
289
|
-
});
|
|
293
|
+
const deps = createMockDependencies();
|
|
290
294
|
await runCommand(['auth', 'clear', 'unknown-service'], deps);
|
|
291
295
|
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);
|
|
296
|
+
expect(errorLogs.length).toBeGreaterThan(0);
|
|
299
297
|
});
|
|
300
298
|
it('should preserve other services when clearing one', async () => {
|
|
301
299
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -303,9 +301,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
303
301
|
slack: { objectType: 'slack', token: 'slack-token', dCookie: 'slack-cookie' },
|
|
304
302
|
discord: { objectType: 'authorizationBare', token: 'discord-token' },
|
|
305
303
|
}));
|
|
306
|
-
const deps = createMockDependencies(
|
|
307
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
308
|
-
});
|
|
304
|
+
const deps = createMockDependencies();
|
|
309
305
|
await runCommand(['auth', 'clear', 'slack'], deps);
|
|
310
306
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
311
307
|
expect(storedData.slack).toBeUndefined();
|
|
@@ -317,23 +313,10 @@ describe('CLI commands with dependency injection', () => {
|
|
|
317
313
|
const browserStatePath = join(tempDir, 'browser_state.json');
|
|
318
314
|
writeSecureFile(storePath, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
|
|
319
315
|
writeSecureFile(browserStatePath, '{}');
|
|
320
|
-
const deps = createMockDependencies(
|
|
321
|
-
config: createMockConfig({ credentialStorePath: storePath, browserStatePath }),
|
|
322
|
-
});
|
|
316
|
+
const deps = createMockDependencies();
|
|
323
317
|
await runCommand(['auth', 'clear', '-y'], deps);
|
|
324
318
|
expect(existsSync(storePath)).toBe(false);
|
|
325
319
|
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
320
|
});
|
|
338
321
|
});
|
|
339
322
|
describe('auth list command', () => {
|
|
@@ -342,9 +325,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
342
325
|
writeSecureFile(storePath, JSON.stringify({
|
|
343
326
|
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
344
327
|
}));
|
|
345
|
-
const deps = createMockDependencies(
|
|
346
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
347
|
-
});
|
|
328
|
+
const deps = createMockDependencies();
|
|
348
329
|
await runCommand(['auth', 'list'], deps);
|
|
349
330
|
expect(logs).toHaveLength(1);
|
|
350
331
|
const entries = JSON.parse(logs[0] ?? '');
|
|
@@ -356,9 +337,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
356
337
|
it('should output empty object when no credentials are stored', async () => {
|
|
357
338
|
const storePath = join(tempDir, 'credentials.json');
|
|
358
339
|
writeSecureFile(storePath, '{}');
|
|
359
|
-
const deps = createMockDependencies(
|
|
360
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
361
|
-
});
|
|
340
|
+
const deps = createMockDependencies();
|
|
362
341
|
await runCommand(['auth', 'list'], deps);
|
|
363
342
|
expect(logs).toHaveLength(1);
|
|
364
343
|
const entries = JSON.parse(logs[0] ?? '');
|
|
@@ -369,9 +348,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
369
348
|
writeSecureFile(storePath, JSON.stringify({
|
|
370
349
|
unknown: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Token: secret'] },
|
|
371
350
|
}));
|
|
372
|
-
const deps = createMockDependencies(
|
|
373
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
374
|
-
});
|
|
351
|
+
const deps = createMockDependencies();
|
|
375
352
|
await runCommand(['auth', 'list'], deps);
|
|
376
353
|
expect(logs).toHaveLength(1);
|
|
377
354
|
const entries = JSON.parse(logs[0] ?? '');
|
|
@@ -385,9 +362,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
385
362
|
it('should store raw curl credentials', async () => {
|
|
386
363
|
const storePath = join(tempDir, 'credentials.json');
|
|
387
364
|
writeSecureFile(storePath, '{}');
|
|
388
|
-
const deps = createMockDependencies(
|
|
389
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
390
|
-
});
|
|
365
|
+
const deps = createMockDependencies();
|
|
391
366
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: secret', '-H', 'X-Other: value'], deps);
|
|
392
367
|
expect(logs).toContain('Credentials stored.');
|
|
393
368
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
@@ -400,30 +375,23 @@ describe('CLI commands with dependency injection', () => {
|
|
|
400
375
|
const deps = createMockDependencies();
|
|
401
376
|
await runCommand(['auth', 'set', 'slack'], deps);
|
|
402
377
|
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
378
|
});
|
|
406
379
|
it('should return error when arguments lack curl switches', async () => {
|
|
407
380
|
const deps = createMockDependencies();
|
|
408
381
|
await runCommand(['auth', 'set', 'slack', 'my-raw-token-value'], deps);
|
|
409
382
|
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
383
|
});
|
|
413
384
|
it('should return error for unknown service', async () => {
|
|
414
385
|
const deps = createMockDependencies();
|
|
415
386
|
await runCommand(['auth', 'set', 'unknown-service', '-H', 'X-Token: secret'], deps);
|
|
416
387
|
expect(exitCode).toBe(1);
|
|
417
|
-
expect(errorLogs.some((log) => log.includes('Unknown service'))).toBe(true);
|
|
418
388
|
});
|
|
419
389
|
it('should overwrite existing credentials', async () => {
|
|
420
390
|
const storePath = join(tempDir, 'credentials.json');
|
|
421
391
|
writeSecureFile(storePath, JSON.stringify({
|
|
422
392
|
slack: { objectType: 'slack', token: 'old-token', dCookie: 'old-cookie' },
|
|
423
393
|
}));
|
|
424
|
-
const deps = createMockDependencies(
|
|
425
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
426
|
-
});
|
|
394
|
+
const deps = createMockDependencies();
|
|
427
395
|
await runCommand(['auth', 'set', 'slack', '-H', 'X-Token: new-secret'], deps);
|
|
428
396
|
expect(logs).toContain('Credentials stored.');
|
|
429
397
|
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
@@ -433,15 +401,53 @@ describe('CLI commands with dependency injection', () => {
|
|
|
433
401
|
});
|
|
434
402
|
});
|
|
435
403
|
});
|
|
404
|
+
describe('auth set-nocurl command', () => {
|
|
405
|
+
it('should store telegram bot credentials', async () => {
|
|
406
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
407
|
+
writeSecureFile(storePath, '{}');
|
|
408
|
+
const deps = createMockDependencies({
|
|
409
|
+
registry: new Registry([TELEGRAM]),
|
|
410
|
+
});
|
|
411
|
+
await runCommand(['auth', 'set-nocurl', 'telegram', '123456:ABC-DEF'], deps);
|
|
412
|
+
expect(logs).toContain('Credentials stored.');
|
|
413
|
+
const storedData = JSON.parse(readSecureFile(storePath) ?? '{}');
|
|
414
|
+
expect(storedData.telegram).toEqual({
|
|
415
|
+
objectType: 'telegramBot',
|
|
416
|
+
token: '123456:ABC-DEF',
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
it('should return error for unknown service', async () => {
|
|
420
|
+
const deps = createMockDependencies();
|
|
421
|
+
await runCommand(['auth', 'set-nocurl', 'unknown-service', 'some-arg'], deps);
|
|
422
|
+
expect(exitCode).toBe(1);
|
|
423
|
+
});
|
|
424
|
+
it('should return error when service does not support set-nocurl', async () => {
|
|
425
|
+
const deps = createMockDependencies();
|
|
426
|
+
await runCommand(['auth', 'set-nocurl', 'slack', 'some-token'], deps);
|
|
427
|
+
expect(exitCode).toBe(1);
|
|
428
|
+
});
|
|
429
|
+
it('should return error when telegram token is missing', async () => {
|
|
430
|
+
const deps = createMockDependencies({
|
|
431
|
+
registry: new Registry([TELEGRAM]),
|
|
432
|
+
});
|
|
433
|
+
await runCommand(['auth', 'set-nocurl', 'telegram'], deps);
|
|
434
|
+
expect(exitCode).toBe(1);
|
|
435
|
+
});
|
|
436
|
+
it('should return error when telegram token format is invalid', async () => {
|
|
437
|
+
const deps = createMockDependencies({
|
|
438
|
+
registry: new Registry([TELEGRAM]),
|
|
439
|
+
});
|
|
440
|
+
await runCommand(['auth', 'set-nocurl', 'telegram', 'not-a-valid-token'], deps);
|
|
441
|
+
expect(exitCode).toBe(1);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
436
444
|
describe('curl command', () => {
|
|
437
445
|
it('should pass arguments to subprocess', async () => {
|
|
438
446
|
const storePath = join(tempDir, 'credentials.json');
|
|
439
447
|
writeSecureFile(storePath, JSON.stringify({
|
|
440
448
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
441
449
|
}));
|
|
442
|
-
const deps = createMockDependencies(
|
|
443
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
444
|
-
});
|
|
450
|
+
const deps = createMockDependencies();
|
|
445
451
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
446
452
|
expect(capturedArgs).toEqual([
|
|
447
453
|
'-H',
|
|
@@ -457,9 +463,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
457
463
|
writeSecureFile(storePath, JSON.stringify({
|
|
458
464
|
slack: { objectType: 'rawCurl', curlArguments: ['-H', 'X-Custom: header'] },
|
|
459
465
|
}));
|
|
460
|
-
const deps = createMockDependencies(
|
|
461
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
462
|
-
});
|
|
466
|
+
const deps = createMockDependencies();
|
|
463
467
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
464
468
|
expect(capturedArgs).toEqual(['-H', 'X-Custom: header', 'https://slack.com/api/test']);
|
|
465
469
|
expect(exitCode).toBe(0);
|
|
@@ -469,9 +473,7 @@ describe('CLI commands with dependency injection', () => {
|
|
|
469
473
|
writeSecureFile(storePath, JSON.stringify({
|
|
470
474
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
471
475
|
}));
|
|
472
|
-
const deps = createMockDependencies(
|
|
473
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
474
|
-
});
|
|
476
|
+
const deps = createMockDependencies();
|
|
475
477
|
await runCommand([
|
|
476
478
|
'curl',
|
|
477
479
|
'--',
|
|
@@ -494,7 +496,6 @@ describe('CLI commands with dependency injection', () => {
|
|
|
494
496
|
slack: { objectType: 'slack', token: 'stored-token', dCookie: 'stored-cookie' },
|
|
495
497
|
}));
|
|
496
498
|
const deps = createMockDependencies({
|
|
497
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
498
499
|
runCurl: () => ({ returncode: 42, stdout: '', stderr: '' }),
|
|
499
500
|
});
|
|
500
501
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
@@ -504,26 +505,11 @@ describe('CLI commands with dependency injection', () => {
|
|
|
504
505
|
const deps = createMockDependencies();
|
|
505
506
|
await runCommand(['curl', '--', '-X', 'POST'], deps);
|
|
506
507
|
expect(exitCode).toBe(1);
|
|
507
|
-
expect(errorLogs.some((log) => log.includes('Could not extract URL'))).toBe(true);
|
|
508
508
|
});
|
|
509
509
|
it('should return error for unknown service', async () => {
|
|
510
510
|
const deps = createMockDependencies();
|
|
511
511
|
await runCommand(['curl', 'https://unknown-api.example.com'], deps);
|
|
512
512
|
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
513
|
});
|
|
528
514
|
it('should read credentials from store and not call login', async () => {
|
|
529
515
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -539,11 +525,16 @@ describe('CLI commands with dependency injection', () => {
|
|
|
539
525
|
info: 'Test info for Slack service.',
|
|
540
526
|
credentialCheckCurlArguments: [],
|
|
541
527
|
checkApiCredentials: vi.fn(),
|
|
528
|
+
setCredentialsExample(serviceName) {
|
|
529
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer xoxb-your-token"`;
|
|
530
|
+
},
|
|
531
|
+
getCredentialsNoCurl() {
|
|
532
|
+
throw new NoCurlCredentialsNotSupportedError('slack');
|
|
533
|
+
},
|
|
542
534
|
getSession: vi.fn().mockReturnValue({ login: mockLogin }),
|
|
543
535
|
};
|
|
544
536
|
const deps = createMockDependencies({
|
|
545
537
|
registry: new Registry([mockSlackService]),
|
|
546
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
547
538
|
});
|
|
548
539
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
549
540
|
expect(mockLogin).not.toHaveBeenCalled();
|
|
@@ -552,14 +543,21 @@ describe('CLI commands with dependency injection', () => {
|
|
|
552
543
|
it('should return error when no credentials in store', async () => {
|
|
553
544
|
const storePath = join(tempDir, 'credentials.json');
|
|
554
545
|
writeSecureFile(storePath, '{}');
|
|
555
|
-
const deps = createMockDependencies(
|
|
556
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
557
|
-
});
|
|
546
|
+
const deps = createMockDependencies();
|
|
558
547
|
await runCommand(['curl', 'https://slack.com/api/test'], deps);
|
|
559
548
|
expect(exitCode).toBe(1);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
549
|
+
});
|
|
550
|
+
it('should inject telegram bot token into URL path', async () => {
|
|
551
|
+
const storePath = join(tempDir, 'credentials.json');
|
|
552
|
+
writeSecureFile(storePath, JSON.stringify({
|
|
553
|
+
telegram: { objectType: 'telegramBot', token: '123456:ABC-DEF' },
|
|
554
|
+
}));
|
|
555
|
+
const deps = createMockDependencies({
|
|
556
|
+
registry: new Registry([TELEGRAM]),
|
|
557
|
+
});
|
|
558
|
+
await runCommand(['curl', 'https://api.telegram.org/getMe'], deps);
|
|
559
|
+
expect(capturedArgs).toEqual(['https://api.telegram.org/bot123456:ABC-DEF/getMe']);
|
|
560
|
+
expect(exitCode).toBe(0);
|
|
563
561
|
});
|
|
564
562
|
it('should work when service does not have getSession but credentials exist', async () => {
|
|
565
563
|
const storePath = join(tempDir, 'credentials.json');
|
|
@@ -574,11 +572,16 @@ describe('CLI commands with dependency injection', () => {
|
|
|
574
572
|
info: 'A service without browser login support.',
|
|
575
573
|
credentialCheckCurlArguments: [],
|
|
576
574
|
checkApiCredentials: vi.fn().mockReturnValue(ApiCredentialStatus.Valid),
|
|
575
|
+
setCredentialsExample(serviceName) {
|
|
576
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
577
|
+
},
|
|
578
|
+
getCredentialsNoCurl() {
|
|
579
|
+
throw new NoCurlCredentialsNotSupportedError('nologin');
|
|
580
|
+
},
|
|
577
581
|
// No getSession - service doesn't support browser login
|
|
578
582
|
};
|
|
579
583
|
const deps = createMockDependencies({
|
|
580
584
|
registry: new Registry([noLoginService]),
|
|
581
|
-
config: createMockConfig({ credentialStorePath: storePath }),
|
|
582
585
|
});
|
|
583
586
|
await runCommand(['curl', 'https://nologin.example.com/api/test'], deps);
|
|
584
587
|
expect(exitCode).toBe(0);
|
|
@@ -596,6 +599,11 @@ describe('CLI commands with dependency injection', () => {
|
|
|
596
599
|
info: 'A service without browser login support.',
|
|
597
600
|
credentialCheckCurlArguments: [],
|
|
598
601
|
checkApiCredentials: vi.fn(),
|
|
602
|
+
setCredentialsExample(serviceName) {
|
|
603
|
+
return `latchkey auth set ${serviceName} -H "Authorization: Bearer <token>"`;
|
|
604
|
+
},
|
|
605
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
606
|
+
getCredentialsNoCurl: Service.prototype.getCredentialsNoCurl,
|
|
599
607
|
// No getSession - service doesn't support browser login
|
|
600
608
|
};
|
|
601
609
|
const deps = createMockDependencies({
|
|
@@ -603,174 +611,66 @@ describe('CLI commands with dependency injection', () => {
|
|
|
603
611
|
});
|
|
604
612
|
await runCommand(['auth', 'browser', 'nologin'], deps);
|
|
605
613
|
expect(exitCode).toBe(1);
|
|
606
|
-
|
|
607
|
-
|
|
614
|
+
});
|
|
615
|
+
it('should suggest set-nocurl when service supports nocurl credentials', async () => {
|
|
616
|
+
const nocurlService = {
|
|
617
|
+
name: 'nocurl-only',
|
|
618
|
+
displayName: 'NoCurl Only Service',
|
|
619
|
+
baseApiUrls: ['https://nocurl.example.com/api/'],
|
|
620
|
+
loginUrl: 'https://nocurl.example.com',
|
|
621
|
+
info: 'A service with nocurl credentials but no browser login.',
|
|
622
|
+
credentialCheckCurlArguments: [],
|
|
623
|
+
checkApiCredentials: vi.fn(),
|
|
624
|
+
setCredentialsExample(serviceName) {
|
|
625
|
+
return `latchkey auth set-nocurl ${serviceName} <some-arg>`;
|
|
626
|
+
},
|
|
627
|
+
getCredentialsNoCurl(arguments_) {
|
|
628
|
+
if (arguments_.length !== 1) {
|
|
629
|
+
throw new Error('Expected exactly one argument');
|
|
630
|
+
}
|
|
631
|
+
return { objectType: 'test', injectIntoCurlCall: vi.fn(), isExpired: () => false };
|
|
632
|
+
},
|
|
633
|
+
// No getSession - service doesn't support browser login
|
|
634
|
+
};
|
|
635
|
+
const deps = createMockDependencies({
|
|
636
|
+
registry: new Registry([nocurlService]),
|
|
637
|
+
});
|
|
638
|
+
await runCommand(['auth', 'browser', 'nocurl-only'], deps);
|
|
639
|
+
expect(exitCode).toBe(1);
|
|
608
640
|
});
|
|
609
641
|
});
|
|
610
642
|
});
|
|
611
|
-
// Integration tests that run the actual CLI binary
|
|
643
|
+
// Integration tests that run the actual CLI binary.
|
|
644
|
+
// Only tests that exercise behavior not covered by the DI unit tests above.
|
|
612
645
|
describe.skipIf(!cliPath)('CLI integration tests (subprocess)', () => {
|
|
613
646
|
let tempDir;
|
|
614
647
|
let testEnv;
|
|
615
648
|
beforeEach(() => {
|
|
616
649
|
tempDir = mkdtempSync(join(tmpdir(), 'latchkey-cli-test-'));
|
|
617
650
|
testEnv = {
|
|
618
|
-
|
|
619
|
-
LATCHKEY_BROWSER_STATE: join(tempDir, 'browser_state.json'),
|
|
651
|
+
LATCHKEY_DIRECTORY: tempDir,
|
|
620
652
|
};
|
|
621
653
|
});
|
|
622
654
|
afterEach(() => {
|
|
623
655
|
rmSync(tempDir, { recursive: true, force: true });
|
|
624
656
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
expect(result.exitCode).toBe(1);
|
|
629
|
-
expect(result.stderr).toContain('Could not extract URL');
|
|
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');
|
|
649
|
-
});
|
|
657
|
+
it('should return non-zero exit code for unknown service URL', () => {
|
|
658
|
+
const result = runCli(['curl', 'https://unknown-api.example.com'], testEnv);
|
|
659
|
+
expect(result.exitCode).toBe(1);
|
|
650
660
|
});
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
LATCHKEY_DISABLE_BROWSER: '1',
|
|
656
|
-
});
|
|
657
|
-
expect(result.exitCode).toBe(1);
|
|
658
|
-
expect(result.stderr).toContain('Browser is disabled');
|
|
661
|
+
it('should return error when browser is disabled via LATCHKEY_DISABLE_BROWSER', () => {
|
|
662
|
+
const result = runCli(['auth', 'browser', 'slack'], {
|
|
663
|
+
...testEnv,
|
|
664
|
+
LATCHKEY_DISABLE_BROWSER: '1',
|
|
659
665
|
});
|
|
666
|
+
expect(result.exitCode).toBe(1);
|
|
660
667
|
});
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
expect(result.exitCode).toBe(0);
|
|
668
|
-
expect(result.stdout).toContain('API credentials for slack have been cleared');
|
|
669
|
-
const storedData = JSON.parse(readSecureFile(testEnv.LATCHKEY_STORE) ?? '{}');
|
|
670
|
-
expect(storedData.slack).toBeUndefined();
|
|
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');
|
|
682
|
-
});
|
|
683
|
-
it('should preserve other services when clearing one', () => {
|
|
684
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({
|
|
685
|
-
slack: { objectType: 'slack', token: 'slack-token', dCookie: 'slack-cookie' },
|
|
686
|
-
discord: { objectType: 'authorizationBare', token: 'discord-token' },
|
|
687
|
-
}));
|
|
688
|
-
const result = runCli(['auth', 'clear', 'slack'], testEnv);
|
|
689
|
-
expect(result.exitCode).toBe(0);
|
|
690
|
-
const storedData = JSON.parse(readSecureFile(testEnv.LATCHKEY_STORE) ?? '{}');
|
|
691
|
-
expect(storedData.slack).toBeUndefined();
|
|
692
|
-
expect(storedData.discord).toBeDefined();
|
|
693
|
-
expect(storedData.discord?.token).toBe('discord-token');
|
|
694
|
-
});
|
|
695
|
-
it('should delete both store and browser state with -y flag', () => {
|
|
696
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({ slack: { objectType: 'slack', token: 'test', dCookie: 'test' } }));
|
|
697
|
-
writeSecureFile(testEnv.LATCHKEY_BROWSER_STATE, '{}');
|
|
698
|
-
const result = runCli(['auth', 'clear', '-y'], testEnv);
|
|
699
|
-
expect(result.exitCode).toBe(0);
|
|
700
|
-
expect(existsSync(testEnv.LATCHKEY_STORE)).toBe(false);
|
|
701
|
-
expect(existsSync(testEnv.LATCHKEY_BROWSER_STATE)).toBe(false);
|
|
702
|
-
expect(result.stdout).toContain(`Deleted credentials store: ${testEnv.LATCHKEY_STORE}`);
|
|
703
|
-
expect(result.stdout).toContain(`Deleted browser state: ${testEnv.LATCHKEY_BROWSER_STATE}`);
|
|
704
|
-
});
|
|
705
|
-
it('should delete only existing files with -y flag', () => {
|
|
706
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
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');
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
describe('auth list command', () => {
|
|
721
|
-
it('should list stored credentials as beautified JSON', () => {
|
|
722
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, JSON.stringify({
|
|
723
|
-
slack: { objectType: 'slack', token: 'test-token', dCookie: 'test-cookie' },
|
|
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);
|
|
738
|
-
});
|
|
739
|
-
});
|
|
740
|
-
describe('services list command', () => {
|
|
741
|
-
it('should list all services as JSON', () => {
|
|
742
|
-
const result = runCli(['services', 'list'], testEnv);
|
|
743
|
-
expect(result.exitCode).toBe(0);
|
|
744
|
-
const services = JSON.parse(result.stdout.trim());
|
|
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');
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
describe('services info command', () => {
|
|
753
|
-
it('should show login options, credentials status, and developer notes as JSON', () => {
|
|
754
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
755
|
-
const result = runCli(['services', 'info', 'slack'], testEnv);
|
|
756
|
-
expect(result.exitCode).toBe(0);
|
|
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));
|
|
761
|
-
});
|
|
762
|
-
it('should show auth set only for services without browser login', () => {
|
|
763
|
-
writeSecureFile(testEnv.LATCHKEY_STORE, '{}');
|
|
764
|
-
const result = runCli(['services', 'info', 'mailchimp'], testEnv);
|
|
765
|
-
expect(result.exitCode).toBe(0);
|
|
766
|
-
const info = JSON.parse(result.stdout);
|
|
767
|
-
expect(info.authOptions).toEqual(['set']);
|
|
768
|
-
});
|
|
769
|
-
it('should return error for unknown service', () => {
|
|
770
|
-
const result = runCli(['services', 'info', 'unknown-service'], testEnv);
|
|
771
|
-
expect(result.exitCode).toBe(1);
|
|
772
|
-
expect(result.stderr).toContain('Unknown service');
|
|
773
|
-
});
|
|
668
|
+
it('should list services as JSON', () => {
|
|
669
|
+
const result = runCli(['services', 'list'], testEnv);
|
|
670
|
+
expect(result.exitCode).toBe(0);
|
|
671
|
+
const services = JSON.parse(result.stdout.trim());
|
|
672
|
+
expect(services).toContain('slack');
|
|
673
|
+
expect(services).toContain('github');
|
|
774
674
|
});
|
|
775
675
|
});
|
|
776
676
|
//# sourceMappingURL=cli.test.js.map
|