@antasphere/chassis-cli 0.0.0-stage → 1.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/LICENSE +82 -0
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +582 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/completion.d.ts +5 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +85 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/demo.d.ts +12 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +210 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/files.d.ts +21 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/files.js +178 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/projects.d.ts +38 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +386 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/teams.d.ts +46 -0
- package/dist/commands/teams.d.ts.map +1 -0
- package/dist/commands/teams.js +265 -0
- package/dist/commands/teams.js.map +1 -0
- package/dist/commands/workspaces.d.ts +22 -0
- package/dist/commands/workspaces.d.ts.map +1 -0
- package/dist/commands/workspaces.js +116 -0
- package/dist/commands/workspaces.js.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +54 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +152 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +456 -0
- package/dist/context.js.map +1 -0
- package/dist/identity.d.ts +36 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +18 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/kit.d.ts +46 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +2 -0
- package/dist/kit.js.map +1 -0
- package/dist/open.d.ts +27 -0
- package/dist/open.d.ts.map +1 -0
- package/dist/open.js +45 -0
- package/dist/open.js.map +1 -0
- package/dist/owner-session.d.ts +34 -0
- package/dist/owner-session.d.ts.map +1 -0
- package/dist/owner-session.js +106 -0
- package/dist/owner-session.js.map +1 -0
- package/dist/program.d.ts +5 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +150 -0
- package/dist/program.js.map +1 -0
- package/dist/safe-write.d.ts +46 -0
- package/dist/safe-write.d.ts.map +1 -0
- package/dist/safe-write.js +127 -0
- package/dist/safe-write.js.map +1 -0
- package/dist/stdin.d.ts +4 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +36 -0
- package/dist/stdin.js.map +1 -0
- package/dist/testing/index.d.ts +70 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +67 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/workspace.d.ts +79 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +139 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +43 -3
- package/src/commands/auth.ts +687 -0
- package/src/commands/completion.ts +102 -0
- package/src/commands/demo.ts +271 -0
- package/src/commands/files.ts +197 -0
- package/src/commands/projects.ts +460 -0
- package/src/commands/teams.ts +320 -0
- package/src/commands/workspaces.ts +140 -0
- package/src/config.ts +81 -0
- package/src/context.ts +616 -0
- package/src/identity.ts +48 -0
- package/src/index.ts +70 -0
- package/src/kit.ts +50 -0
- package/src/open.ts +48 -0
- package/src/owner-session.ts +152 -0
- package/src/program.ts +183 -0
- package/src/safe-write.ts +136 -0
- package/src/stdin.ts +39 -0
- package/src/testing/index.ts +129 -0
- package/src/workspace.ts +229 -0
- package/test/host/identity.test.ts +229 -0
- package/test/host/minimal-host.ts +16 -0
- package/test/host/minimal-tool.ts +40 -0
- package/test/suite/cli.test.ts +301 -0
- package/test/suite/config.test.ts +59 -0
- package/test/suite/demo.test.ts +467 -0
- package/test/suite/hub-profile-shared.test.ts +123 -0
- package/test/suite/login.test.ts +743 -0
- package/test/suite/logout.test.ts +247 -0
- package/test/suite/migration.test.ts +109 -0
- package/test/suite/open.test.ts +59 -0
- package/test/suite/profiles.test.ts +380 -0
- package/test/suite/projects.test.ts +550 -0
- package/test/suite/recovery.test.ts +348 -0
- package/test/suite/security.test.ts +199 -0
- package/test/suite/signin-fixtures.ts +204 -0
- package/test/suite/teams.test.ts +354 -0
- package/test/suite/whoami.test.ts +159 -0
- package/test/suite/workspace.test.ts +402 -0
- package/README.md +0 -3
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { HUB_TOOL, loadConfig as loadCoreConfig, saveConfig as saveCoreConfig } from '@antasphere/cli-core';
|
|
3
|
+
import { redactKey } from '@antasphere/chassis-cli';
|
|
4
|
+
import { routedHarness, tempConfigEnv, type Route } from '@antasphere/chassis-cli/testing';
|
|
5
|
+
import { cli, run } from '@chassis-cli-test/host';
|
|
6
|
+
import {
|
|
7
|
+
cloudInstanceRoute,
|
|
8
|
+
exchangeRoutes,
|
|
9
|
+
HUB,
|
|
10
|
+
HUB_KEY,
|
|
11
|
+
key,
|
|
12
|
+
meBody,
|
|
13
|
+
meRoute,
|
|
14
|
+
ORG,
|
|
15
|
+
ossInstanceRoute,
|
|
16
|
+
otpRoutes,
|
|
17
|
+
seedHubLogin,
|
|
18
|
+
TOOL_ACCESS_DENIED,
|
|
19
|
+
wsRow
|
|
20
|
+
} from './signin-fixtures.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ONE `login` that ends in the tool (PRDCT-2947): a pasted key, the
|
|
24
|
+
* instance's email code (self-hosted), or the Antasphere sign-in then the
|
|
25
|
+
* exchange (cloud). The profile it signs in on is the instance's, and it
|
|
26
|
+
* becomes the active one.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { configPath, loadConfig, saveConfig } = cli;
|
|
30
|
+
const { envPrefix: P, keyPrefix: K, displayName, cloudUrl } = cli.identity;
|
|
31
|
+
|
|
32
|
+
/** A scripted prompt: answers in order, questions recorded. */
|
|
33
|
+
function scripted(h: ReturnType<typeof routedHarness>, answers: string[]): string[] {
|
|
34
|
+
const asked: string[] = [];
|
|
35
|
+
h.io.prompt = async (q) => {
|
|
36
|
+
asked.push(q);
|
|
37
|
+
return answers.shift() ?? '';
|
|
38
|
+
};
|
|
39
|
+
return asked;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A second Antasphere account's hub key (PRDCT-3032). */
|
|
43
|
+
const BOB_HUB_KEY = 'ant_bobkey12_secretsecretsecret1234';
|
|
44
|
+
|
|
45
|
+
/** The routes as given, every answer that names a person naming `email` instead. */
|
|
46
|
+
function answeringAs(email: string, routes: Route[]): Route[] {
|
|
47
|
+
return routes.map((r) => ({
|
|
48
|
+
...r,
|
|
49
|
+
reply: (call) => {
|
|
50
|
+
const answer = r.reply(call);
|
|
51
|
+
const body = answer.body as Record<string, unknown> | undefined;
|
|
52
|
+
if (!body || typeof body !== 'object' || !('user' in body)) return answer;
|
|
53
|
+
return { ...answer, body: { ...body, user: { ...(body.user as object), email } } };
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('login with a pasted key', () => {
|
|
59
|
+
it('verifies the key with /me, saves it on the instance profile and makes it active', async () => {
|
|
60
|
+
const env = await tempConfigEnv();
|
|
61
|
+
const pasted = key('paste');
|
|
62
|
+
const h = routedHarness([meRoute()], env);
|
|
63
|
+
expect(await run(['login', '--api-key', pasted, '--api-url', 'http://inst'], h.io)).toBe(0);
|
|
64
|
+
expect(h.err()).toBe('');
|
|
65
|
+
expect(h.wire).toEqual([
|
|
66
|
+
{ method: 'GET', origin: 'http://inst', path: '/api/v1/me', auth: `Bearer ${pasted}` }
|
|
67
|
+
]);
|
|
68
|
+
expect(h.out()).toBe(
|
|
69
|
+
`Signed in as Ada <ada@x.co> in Acme on http://inst.\nKey saved to profile "inst" (${configPath(env)}).\n`
|
|
70
|
+
);
|
|
71
|
+
const config = loadConfig(env);
|
|
72
|
+
expect(config.activeProfile).toBe('inst');
|
|
73
|
+
expect(config.profiles).toEqual({ inst: { apiKey: pasted, baseUrl: 'http://inst', email: 'ada@x.co' } });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it(`${P}_API_KEY is never taken as a paste: on a self-hosted instance the email code runs (verifier round 1, F8)`, async () => {
|
|
77
|
+
const env = await tempConfigEnv();
|
|
78
|
+
const inShell = key('envkey');
|
|
79
|
+
const minted = key('minted');
|
|
80
|
+
const h = routedHarness([ossInstanceRoute, ...otpRoutes(minted), meRoute()], {
|
|
81
|
+
...env,
|
|
82
|
+
[`${P}_API_KEY`]: inShell
|
|
83
|
+
});
|
|
84
|
+
scripted(h, ['123456']);
|
|
85
|
+
expect(await run(['login', '--api-url', 'http://inst', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
86
|
+
expect(h.wire.map((c) => `${c.method} ${c.path} ${c.auth ?? '-'}`)).toEqual([
|
|
87
|
+
'GET /api/v1/instance -',
|
|
88
|
+
'POST /api/v1/cli/auth/request -',
|
|
89
|
+
'POST /api/v1/cli/auth/complete -',
|
|
90
|
+
`GET /api/v1/me Bearer ${minted}`
|
|
91
|
+
]);
|
|
92
|
+
expect(loadConfig(env).profiles.inst).toEqual({
|
|
93
|
+
apiKey: minted,
|
|
94
|
+
baseUrl: 'http://inst',
|
|
95
|
+
email: 'ada@x.co'
|
|
96
|
+
});
|
|
97
|
+
expect(JSON.stringify(loadConfig(env))).not.toContain(inShell);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it(`${P}_API_KEY is never taken as a paste: on the cloud the Antasphere path runs`, async () => {
|
|
101
|
+
const env = { ...(await tempConfigEnv()), ANTASPHERE_URL: HUB };
|
|
102
|
+
seedHubLogin(env);
|
|
103
|
+
const inShell = key('envkey');
|
|
104
|
+
const minted = key('tool');
|
|
105
|
+
const h = routedHarness([cloudInstanceRoute, ...exchangeRoutes([minted]), meRoute()], {
|
|
106
|
+
...env,
|
|
107
|
+
[`${P}_API_KEY`]: inShell
|
|
108
|
+
});
|
|
109
|
+
expect(await run(['login', '--api-url', 'http://tool', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
110
|
+
expect(h.wire.map((c) => `${c.method} ${c.origin}${c.path} ${c.auth ?? '-'}`)).toEqual([
|
|
111
|
+
'GET http://tool/api/v1/instance -',
|
|
112
|
+
'GET http://tool/api/v1/instance -',
|
|
113
|
+
`POST ${HUB}/api/v1/sso/tool-token Bearer ${HUB_KEY}`,
|
|
114
|
+
'POST http://tool/api/v1/sso/cli-connect -',
|
|
115
|
+
`GET http://tool/api/v1/me Bearer ${minted}`
|
|
116
|
+
]);
|
|
117
|
+
const tool = loadConfig(env).profiles.tool;
|
|
118
|
+
expect(tool?.apiKey).toBeUndefined();
|
|
119
|
+
expect(tool?.connectKeys?.default?.apiKey).toBe(minted);
|
|
120
|
+
expect(JSON.stringify(loadConfig(env))).not.toContain(inShell);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('reads the key from --api-key-stdin', async () => {
|
|
124
|
+
const env = await tempConfigEnv();
|
|
125
|
+
const pasted = key('stdin');
|
|
126
|
+
const h = routedHarness([meRoute()], env);
|
|
127
|
+
h.io.readStdin = async () => `${pasted}\n`;
|
|
128
|
+
expect(await run(['login', '--api-key-stdin', '--api-url', 'http://inst'], h.io)).toBe(0);
|
|
129
|
+
expect(h.wire[0]?.auth).toBe(`Bearer ${pasted}`);
|
|
130
|
+
expect(loadConfig(env).profiles.inst?.apiKey).toBe(pasted);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('a person with no name reads as the email alone', async () => {
|
|
134
|
+
const env = await tempConfigEnv();
|
|
135
|
+
const h = routedHarness([meRoute(meBody({ name: '' }))], env);
|
|
136
|
+
expect(await run(['login', '--api-key', key('paste'), '--api-url', 'http://inst'], h.io)).toBe(0);
|
|
137
|
+
expect(h.out().split('\n')[0]).toBe('Signed in as ada@x.co in Acme on http://inst.');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it(`refuses a key that is not a ${displayName} key, before any request`, async () => {
|
|
141
|
+
const env = await tempConfigEnv();
|
|
142
|
+
const h = routedHarness([meRoute()], env);
|
|
143
|
+
expect(await run(['login', '--api-key', 'zzz_not_ours', '--api-url', 'http://inst'], h.io)).toBe(1);
|
|
144
|
+
expect(h.err()).toBe(`Error: That is not a ${displayName} key: it should start with ${K}_.\n`);
|
|
145
|
+
expect(h.wire).toEqual([]);
|
|
146
|
+
expect(loadConfig(env)).toEqual({ profiles: {} });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('a key the instance refuses is never saved', async () => {
|
|
150
|
+
const env = await tempConfigEnv();
|
|
151
|
+
const h = routedHarness(
|
|
152
|
+
[
|
|
153
|
+
{
|
|
154
|
+
method: 'GET',
|
|
155
|
+
path: /\/api\/v1\/me$/,
|
|
156
|
+
reply: () => ({
|
|
157
|
+
status: 401,
|
|
158
|
+
body: { error: { code: 'invalid_api_key', message: 'API key not recognized' } }
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
env
|
|
163
|
+
);
|
|
164
|
+
expect(await run(['login', '--api-key', key('bad'), '--api-url', 'http://inst'], h.io)).toBe(1);
|
|
165
|
+
expect(loadConfig(env)).toEqual({ profiles: {} });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('--json: the profile, the URL, how, who, where, and the file', async () => {
|
|
169
|
+
const env = await tempConfigEnv();
|
|
170
|
+
const pasted = key('paste');
|
|
171
|
+
const me = meBody();
|
|
172
|
+
const h = routedHarness([meRoute(me)], env);
|
|
173
|
+
expect(await run(['login', '--json', '--api-key', pasted, '--api-url', 'http://inst'], h.io)).toBe(0);
|
|
174
|
+
expect(JSON.parse(h.out())).toEqual({
|
|
175
|
+
profile: 'inst',
|
|
176
|
+
baseUrl: 'http://inst',
|
|
177
|
+
via: 'key',
|
|
178
|
+
user: me.user,
|
|
179
|
+
workspace: me.workspace,
|
|
180
|
+
configPath: configPath(env)
|
|
181
|
+
});
|
|
182
|
+
expect(h.out()).not.toContain(pasted);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('--profile names the profile the key is saved on', async () => {
|
|
186
|
+
const env = await tempConfigEnv();
|
|
187
|
+
saveConfig(env, { profiles: { work: { baseUrl: 'http://inst' } } });
|
|
188
|
+
const h = routedHarness([meRoute()], env);
|
|
189
|
+
expect(await run(['login', '--api-key', key('paste'), '--profile', 'work'], h.io)).toBe(0);
|
|
190
|
+
expect(loadConfig(env)).toEqual({
|
|
191
|
+
activeProfile: 'work',
|
|
192
|
+
profiles: { work: { apiKey: key('paste'), baseUrl: 'http://inst', email: 'ada@x.co' } }
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('login on a self-hosted instance: the email code', () => {
|
|
198
|
+
it('asks the email and the code, mints the key, saves it on the instance profile', async () => {
|
|
199
|
+
const env = await tempConfigEnv();
|
|
200
|
+
const minted = key('minted');
|
|
201
|
+
const h = routedHarness([ossInstanceRoute, ...otpRoutes(minted), meRoute()], env);
|
|
202
|
+
const asked = scripted(h, ['ada@x.co', '123456']);
|
|
203
|
+
expect(await run(['login', '--api-url', 'http://inst'], h.io)).toBe(0);
|
|
204
|
+
expect(asked).toEqual(['Email: ', 'Code: ']);
|
|
205
|
+
expect(h.wire).toEqual([
|
|
206
|
+
{ method: 'GET', origin: 'http://inst', path: '/api/v1/instance', auth: undefined },
|
|
207
|
+
{
|
|
208
|
+
method: 'POST',
|
|
209
|
+
origin: 'http://inst',
|
|
210
|
+
path: '/api/v1/cli/auth/request',
|
|
211
|
+
auth: undefined,
|
|
212
|
+
body: { email: 'ada@x.co' }
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
method: 'POST',
|
|
216
|
+
origin: 'http://inst',
|
|
217
|
+
path: '/api/v1/cli/auth/complete',
|
|
218
|
+
auth: undefined,
|
|
219
|
+
body: { email: 'ada@x.co', otp: '123456' }
|
|
220
|
+
},
|
|
221
|
+
{ method: 'GET', origin: 'http://inst', path: '/api/v1/me', auth: `Bearer ${minted}` }
|
|
222
|
+
]);
|
|
223
|
+
expect(h.err()).toBe('A sign-in code was sent to ada@x.co (if that account exists on http://inst).\n');
|
|
224
|
+
expect(h.out()).toBe(
|
|
225
|
+
'Signed in as Ada <ada@x.co> in Acme on http://inst.\n' +
|
|
226
|
+
`API key "CLI login" (${redactKey(minted)}) saved to profile "inst" (${configPath(env)}).\n`
|
|
227
|
+
);
|
|
228
|
+
expect(h.out()).not.toContain(minted);
|
|
229
|
+
const config = loadConfig(env);
|
|
230
|
+
expect(config.activeProfile).toBe('inst');
|
|
231
|
+
expect(config.profiles.inst).toEqual({ apiKey: minted, baseUrl: 'http://inst', email: 'ada@x.co' });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('--email skips the first question; --key-name and --expires-in-days reach the mint', async () => {
|
|
235
|
+
const env = await tempConfigEnv();
|
|
236
|
+
const h = routedHarness([ossInstanceRoute, ...otpRoutes(key('minted')), meRoute()], env);
|
|
237
|
+
const asked = scripted(h, ['654321']);
|
|
238
|
+
expect(
|
|
239
|
+
await run(
|
|
240
|
+
[
|
|
241
|
+
'login',
|
|
242
|
+
'--api-url',
|
|
243
|
+
'http://inst',
|
|
244
|
+
'--email',
|
|
245
|
+
'ada@x.co',
|
|
246
|
+
'--key-name',
|
|
247
|
+
'laptop',
|
|
248
|
+
'--expires-in-days',
|
|
249
|
+
'30'
|
|
250
|
+
],
|
|
251
|
+
h.io
|
|
252
|
+
)
|
|
253
|
+
).toBe(0);
|
|
254
|
+
expect(asked).toEqual(['Code: ']);
|
|
255
|
+
expect(h.wire.find((c) => c.path.endsWith('/complete'))?.body).toEqual({
|
|
256
|
+
email: 'ada@x.co',
|
|
257
|
+
otp: '654321',
|
|
258
|
+
keyName: 'laptop',
|
|
259
|
+
expiresInDays: 30
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('--json never carries the key', async () => {
|
|
264
|
+
const env = await tempConfigEnv();
|
|
265
|
+
const minted = key('minted');
|
|
266
|
+
const me = meBody();
|
|
267
|
+
const h = routedHarness([ossInstanceRoute, ...otpRoutes(minted), meRoute(me)], env);
|
|
268
|
+
scripted(h, ['123456']);
|
|
269
|
+
expect(await run(['login', '--json', '--api-url', 'http://inst', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
270
|
+
expect(JSON.parse(h.out())).toEqual({
|
|
271
|
+
profile: 'inst',
|
|
272
|
+
baseUrl: 'http://inst',
|
|
273
|
+
via: 'otp',
|
|
274
|
+
user: me.user,
|
|
275
|
+
workspace: me.workspace,
|
|
276
|
+
keyName: 'CLI login',
|
|
277
|
+
configPath: configPath(env)
|
|
278
|
+
});
|
|
279
|
+
expect(h.out()).not.toContain(minted);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('an instance whose discovery does not answer is signed in the self-hosted way', async () => {
|
|
283
|
+
const env = await tempConfigEnv();
|
|
284
|
+
const h = routedHarness([...otpRoutes(key('minted')), meRoute()], env);
|
|
285
|
+
scripted(h, ['123456']);
|
|
286
|
+
expect(await run(['login', '--api-url', 'http://inst', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
287
|
+
expect(h.wire.map((c) => c.path)).toEqual([
|
|
288
|
+
'/api/v1/instance',
|
|
289
|
+
'/api/v1/cli/auth/request',
|
|
290
|
+
'/api/v1/cli/auth/complete',
|
|
291
|
+
'/api/v1/me'
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('with no prompt wired, the question is a clean error and nothing is requested', async () => {
|
|
296
|
+
const env = await tempConfigEnv();
|
|
297
|
+
const h = routedHarness([ossInstanceRoute, ...otpRoutes(key('minted')), meRoute()], env);
|
|
298
|
+
expect(await run(['login', '--api-url', 'http://inst'], h.io)).toBe(1);
|
|
299
|
+
expect(h.err()).toBe(
|
|
300
|
+
'Error: This command is interactive — no prompt available (pass the value as a flag).\n'
|
|
301
|
+
);
|
|
302
|
+
expect(h.wire.map((c) => c.path)).toEqual(['/api/v1/instance']);
|
|
303
|
+
expect(loadConfig(env)).toEqual({ profiles: {} });
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('login on the cloud: the Antasphere sign-in, then the exchange', () => {
|
|
308
|
+
const hubEnv = async () => ({ ...(await tempConfigEnv()), ANTASPHERE_URL: HUB });
|
|
309
|
+
const ORG_ROWS = [
|
|
310
|
+
wsRow('w1', 'Acme', { default: true, hubOrigin: true, centralAccountId: ORG }),
|
|
311
|
+
wsRow('w2', 'Beta', { hubOrigin: true, centralAccountId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee' })
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
it('with no Antasphere login: the hub email code, the hub profile written as the hub CLI writes it, then the exchange', async () => {
|
|
315
|
+
const env = await hubEnv();
|
|
316
|
+
const minted = key('tool');
|
|
317
|
+
const h = routedHarness(
|
|
318
|
+
[
|
|
319
|
+
cloudInstanceRoute,
|
|
320
|
+
...otpRoutes(HUB_KEY, ORG),
|
|
321
|
+
...exchangeRoutes([minted]),
|
|
322
|
+
meRoute(meBody({ workspaces: ORG_ROWS }))
|
|
323
|
+
],
|
|
324
|
+
env
|
|
325
|
+
);
|
|
326
|
+
const asked = scripted(h, ['ada@x.co', '123456']);
|
|
327
|
+
expect(await run(['login', '--api-url', 'http://tool'], h.io)).toBe(0);
|
|
328
|
+
expect(asked).toEqual(['Antasphere email: ', 'Code: ']);
|
|
329
|
+
expect(h.wire.map((c) => `${c.method} ${c.origin}${c.path} ${c.auth ?? '-'}`)).toEqual([
|
|
330
|
+
'GET http://tool/api/v1/instance -',
|
|
331
|
+
`POST ${HUB}/api/v1/cli/auth/request -`,
|
|
332
|
+
`POST ${HUB}/api/v1/cli/auth/complete -`,
|
|
333
|
+
'GET http://tool/api/v1/instance -',
|
|
334
|
+
`POST ${HUB}/api/v1/sso/tool-token Bearer ${HUB_KEY}`,
|
|
335
|
+
'POST http://tool/api/v1/sso/cli-connect -',
|
|
336
|
+
`GET http://tool/api/v1/me Bearer ${minted}`
|
|
337
|
+
]);
|
|
338
|
+
expect(h.wire[2]?.body).toEqual({ email: 'ada@x.co', otp: '123456' });
|
|
339
|
+
expect(h.err()).toContain(
|
|
340
|
+
'A sign-in code was sent to ada@x.co (if that Antasphere account can receive mail).\n'
|
|
341
|
+
);
|
|
342
|
+
expect(h.out()).toBe(
|
|
343
|
+
`Signed in as Ada <ada@x.co> in Acme (organization ${ORG}) on http://tool.\n` +
|
|
344
|
+
'Your workspaces: Acme, Beta. Pass --org <name> (or --workspace <name>) to work in another.\n'
|
|
345
|
+
);
|
|
346
|
+
// The hub profile: exactly what `antasphere login` writes, named the hub
|
|
347
|
+
// CLI's way (cli-core `selectProfile`: ANTASPHERE_URL=http://hub makes
|
|
348
|
+
// the profile of that host, `hub`).
|
|
349
|
+
expect(loadCoreConfig(env, HUB_TOOL)).toEqual({
|
|
350
|
+
activeProfile: 'hub',
|
|
351
|
+
profiles: { hub: { apiKey: HUB_KEY, baseUrl: HUB, email: 'ada@x.co', workspaceId: ORG } }
|
|
352
|
+
});
|
|
353
|
+
// The tool's key is cached on the instance profile, which is the active one.
|
|
354
|
+
const config = loadConfig(env);
|
|
355
|
+
expect(config.activeProfile).toBe('tool');
|
|
356
|
+
expect(config.profiles.tool?.baseUrl).toBe('http://tool');
|
|
357
|
+
expect(config.profiles.tool?.apiKey).toBeUndefined();
|
|
358
|
+
expect(config.profiles.tool?.connectKeys?.hub).toMatchObject({ apiKey: minted, email: 'ada@x.co' });
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('a clean machine: the hub profile `cloud` at account.antasphere.com', async () => {
|
|
362
|
+
const env = await tempConfigEnv();
|
|
363
|
+
const h = routedHarness(
|
|
364
|
+
[cloudInstanceRoute, ...otpRoutes(HUB_KEY, ORG), ...exchangeRoutes([key('tool')]), meRoute()],
|
|
365
|
+
env
|
|
366
|
+
);
|
|
367
|
+
scripted(h, ['123456']);
|
|
368
|
+
expect(await run(['login', '--api-url', 'http://tool', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
369
|
+
expect(h.wire[1]).toMatchObject({
|
|
370
|
+
origin: 'https://account.antasphere.com',
|
|
371
|
+
path: '/api/v1/cli/auth/request'
|
|
372
|
+
});
|
|
373
|
+
expect(loadCoreConfig(env, HUB_TOOL)).toEqual({
|
|
374
|
+
activeProfile: 'cloud',
|
|
375
|
+
profiles: {
|
|
376
|
+
cloud: {
|
|
377
|
+
apiKey: HUB_KEY,
|
|
378
|
+
baseUrl: 'https://account.antasphere.com',
|
|
379
|
+
email: 'ada@x.co',
|
|
380
|
+
workspaceId: ORG
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
expect(loadConfig(env).profiles.tool?.connectKeys?.cloud?.apiKey).toBe(key('tool'));
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('a stored hub login the hub rejects, with no ANTASPHERE_URL: re-signs in at the STORED hub URL, and the profile keeps its other fields', async () => {
|
|
388
|
+
const env = await tempConfigEnv();
|
|
389
|
+
saveCoreConfig(env, HUB_TOOL, {
|
|
390
|
+
activeProfile: 'default',
|
|
391
|
+
profiles: {
|
|
392
|
+
default: {
|
|
393
|
+
apiKey: HUB_KEY,
|
|
394
|
+
baseUrl: HUB,
|
|
395
|
+
email: 'ada@x.co',
|
|
396
|
+
connectKeys: { other: { apiKey: 'kept' } },
|
|
397
|
+
// A field this CLI does not know: the hub CLI's spread keeps it, so must this one.
|
|
398
|
+
label: 'mine'
|
|
399
|
+
} as Record<string, unknown>,
|
|
400
|
+
spare: { baseUrl: 'http://spare' }
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
const fresh = 'ant_freshkey_secretsecretsecret1234';
|
|
404
|
+
const h = routedHarness(
|
|
405
|
+
[
|
|
406
|
+
cloudInstanceRoute,
|
|
407
|
+
...otpRoutes(fresh, ORG),
|
|
408
|
+
...exchangeRoutes([key('tool')], {
|
|
409
|
+
toolToken: (n) =>
|
|
410
|
+
n === 1
|
|
411
|
+
? { status: 401, body: { error: { code: 'unauthenticated', message: 'bad key' } } }
|
|
412
|
+
: undefined
|
|
413
|
+
}),
|
|
414
|
+
meRoute()
|
|
415
|
+
],
|
|
416
|
+
env
|
|
417
|
+
);
|
|
418
|
+
scripted(h, ['123456']);
|
|
419
|
+
expect(await run(['login', '--api-url', 'http://tool', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
420
|
+
expect(h.err()).toContain('The stored Antasphere login was rejected: signing in again.\n');
|
|
421
|
+
expect(h.wire.filter((c) => c.path.startsWith('/api/v1/cli/auth/')).map((c) => c.origin)).toEqual([
|
|
422
|
+
HUB,
|
|
423
|
+
HUB
|
|
424
|
+
]);
|
|
425
|
+
expect(h.wire.some((c) => c.origin === 'https://account.antasphere.com')).toBe(false);
|
|
426
|
+
expect(loadCoreConfig(env, HUB_TOOL)).toEqual({
|
|
427
|
+
activeProfile: 'default',
|
|
428
|
+
profiles: {
|
|
429
|
+
default: {
|
|
430
|
+
apiKey: fresh,
|
|
431
|
+
baseUrl: HUB,
|
|
432
|
+
email: 'ada@x.co',
|
|
433
|
+
workspaceId: ORG,
|
|
434
|
+
connectKeys: { other: { apiKey: 'kept' } },
|
|
435
|
+
label: 'mine'
|
|
436
|
+
},
|
|
437
|
+
spare: { baseUrl: 'http://spare' }
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
expect(loadConfig(env).profiles.tool?.connectKeys?.default?.apiKey).toBe(key('tool'));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('a login revokes and replaces a cached key the instance no longer accepts (verifier round 1, F3)', async () => {
|
|
444
|
+
const env = await hubEnv();
|
|
445
|
+
seedHubLogin(env);
|
|
446
|
+
const dead = key('dead');
|
|
447
|
+
const minted = key('tool');
|
|
448
|
+
saveConfig(env, {
|
|
449
|
+
activeProfile: 'tool',
|
|
450
|
+
profiles: { tool: { baseUrl: 'http://tool', connectKeys: { default: { apiKey: dead } } } }
|
|
451
|
+
});
|
|
452
|
+
const refused = {
|
|
453
|
+
status: 401,
|
|
454
|
+
body: { error: { code: 'invalid_api_key', message: 'API key not recognized' } }
|
|
455
|
+
};
|
|
456
|
+
const bearer = (headers: Headers) => headers.get('authorization')?.replace(/^Bearer\s+/, '');
|
|
457
|
+
const h = routedHarness(
|
|
458
|
+
[
|
|
459
|
+
cloudInstanceRoute,
|
|
460
|
+
{ method: 'DELETE', path: /\/api\/v1\/cli\/auth\/key$/, reply: () => refused },
|
|
461
|
+
...exchangeRoutes([minted]),
|
|
462
|
+
{
|
|
463
|
+
method: 'GET',
|
|
464
|
+
path: /\/api\/v1\/me$/,
|
|
465
|
+
reply: ({ headers }) => (bearer(headers) === minted ? { body: meBody() } : refused)
|
|
466
|
+
}
|
|
467
|
+
],
|
|
468
|
+
env
|
|
469
|
+
);
|
|
470
|
+
expect(await run(['login', '--api-url', 'http://tool'], h.io)).toBe(0);
|
|
471
|
+
expect(h.wire.map((c) => `${c.method} ${c.origin}${c.path} ${c.auth ?? '-'}`)).toEqual([
|
|
472
|
+
'GET http://tool/api/v1/instance -',
|
|
473
|
+
`DELETE http://tool/api/v1/cli/auth/key Bearer ${dead}`,
|
|
474
|
+
'GET http://tool/api/v1/instance -',
|
|
475
|
+
`POST ${HUB}/api/v1/sso/tool-token Bearer ${HUB_KEY}`,
|
|
476
|
+
'POST http://tool/api/v1/sso/cli-connect -',
|
|
477
|
+
`GET http://tool/api/v1/me Bearer ${minted}`
|
|
478
|
+
]);
|
|
479
|
+
const tool = loadConfig(env).profiles.tool;
|
|
480
|
+
expect(Object.keys(tool?.connectKeys ?? {})).toEqual(['default']);
|
|
481
|
+
expect(tool?.connectKeys?.default?.apiKey).toBe(minted);
|
|
482
|
+
expect(JSON.stringify(loadConfig(env))).not.toContain(dead);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('a login with a live cached key revokes it too, and ends with one new key on the profile', async () => {
|
|
486
|
+
const env = await hubEnv();
|
|
487
|
+
seedHubLogin(env);
|
|
488
|
+
const live = key('live');
|
|
489
|
+
const minted = key('tool');
|
|
490
|
+
saveConfig(env, {
|
|
491
|
+
activeProfile: 'tool',
|
|
492
|
+
profiles: { tool: { baseUrl: 'http://tool', connectKeys: { default: { apiKey: live } } } }
|
|
493
|
+
});
|
|
494
|
+
const h = routedHarness(
|
|
495
|
+
[
|
|
496
|
+
cloudInstanceRoute,
|
|
497
|
+
{ method: 'DELETE', path: /\/api\/v1\/cli\/auth\/key$/, reply: () => ({ body: { revoked: true } }) },
|
|
498
|
+
...exchangeRoutes([minted]),
|
|
499
|
+
meRoute()
|
|
500
|
+
],
|
|
501
|
+
env
|
|
502
|
+
);
|
|
503
|
+
expect(await run(['login', '--api-url', 'http://tool'], h.io)).toBe(0);
|
|
504
|
+
expect(h.wire.filter((c) => c.method === 'DELETE')).toEqual([
|
|
505
|
+
{ method: 'DELETE', origin: 'http://tool', path: '/api/v1/cli/auth/key', auth: `Bearer ${live}` }
|
|
506
|
+
]);
|
|
507
|
+
expect(h.wire.filter((c) => c.path.endsWith('/sso/cli-connect'))).toHaveLength(1);
|
|
508
|
+
expect(h.wire.at(-1)).toMatchObject({ path: '/api/v1/me', auth: `Bearer ${minted}` });
|
|
509
|
+
expect(loadConfig(env).profiles.tool?.connectKeys).toEqual({
|
|
510
|
+
default: expect.objectContaining({ apiKey: minted })
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('F9: --profile naming another instance than --api-url is refused before any request, and nothing moves', async () => {
|
|
515
|
+
const env = await hubEnv();
|
|
516
|
+
seedHubLogin(env);
|
|
517
|
+
const cached = key('cached');
|
|
518
|
+
saveConfig(env, {
|
|
519
|
+
activeProfile: 'work',
|
|
520
|
+
profiles: { work: { baseUrl: 'http://tool', connectKeys: { default: { apiKey: cached } } } }
|
|
521
|
+
});
|
|
522
|
+
const before = JSON.stringify(loadConfig(env));
|
|
523
|
+
const h = routedHarness([cloudInstanceRoute, ...exchangeRoutes([key('tool')]), meRoute()], env);
|
|
524
|
+
expect(await run(['login', '--profile', 'work', '--api-url', 'http://other'], h.io)).toBe(1);
|
|
525
|
+
expect(h.wire).toEqual([]);
|
|
526
|
+
expect(h.err()).toBe(
|
|
527
|
+
'Error: Profile "work" is the instance http://tool, and this login targets http://other. ' +
|
|
528
|
+
'Drop --profile to sign in on the profile of http://other, or drop --api-url to sign in on "work".\n'
|
|
529
|
+
);
|
|
530
|
+
expect(JSON.stringify(loadConfig(env))).toBe(before);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('F9: the same refusal for a pasted key: the profile is never repointed and the key goes nowhere', async () => {
|
|
534
|
+
const env = await hubEnv();
|
|
535
|
+
const own = `${K}_ownkey12_0123456789abcdefghij`;
|
|
536
|
+
const pasted = `${K}_pasted12_0123456789abcdefghij`;
|
|
537
|
+
saveConfig(env, { activeProfile: 'work', profiles: { work: { baseUrl: 'http://tool', apiKey: own } } });
|
|
538
|
+
const h = routedHarness([meRoute()], env);
|
|
539
|
+
expect(
|
|
540
|
+
await run(['login', '--profile', 'work', '--api-url', 'http://other', '--api-key', pasted], h.io)
|
|
541
|
+
).toBe(1);
|
|
542
|
+
expect(h.wire).toEqual([]);
|
|
543
|
+
expect(loadConfig(env).profiles.work).toEqual({ baseUrl: 'http://tool', apiKey: own });
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('the hub profile keeps the name the hub CLI made active', async () => {
|
|
547
|
+
const env = await hubEnv();
|
|
548
|
+
saveCoreConfig(env, HUB_TOOL, { activeProfile: 'work', profiles: { work: { baseUrl: HUB } } });
|
|
549
|
+
const h = routedHarness(
|
|
550
|
+
[cloudInstanceRoute, ...otpRoutes(HUB_KEY, ORG), ...exchangeRoutes([key('tool')]), meRoute()],
|
|
551
|
+
env
|
|
552
|
+
);
|
|
553
|
+
scripted(h, ['123456']);
|
|
554
|
+
expect(await run(['login', '--api-url', 'http://tool', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
555
|
+
expect(loadCoreConfig(env, HUB_TOOL)).toEqual({
|
|
556
|
+
activeProfile: 'work',
|
|
557
|
+
profiles: { work: { apiKey: HUB_KEY, baseUrl: HUB, email: 'ada@x.co', workspaceId: ORG } }
|
|
558
|
+
});
|
|
559
|
+
expect(loadConfig(env).profiles.tool?.connectKeys?.work?.apiKey).toBe(key('tool'));
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('with no URL at all, signs in on the implicit cloud profile at the cloud URL', async () => {
|
|
563
|
+
const env = await hubEnv();
|
|
564
|
+
seedHubLogin(env);
|
|
565
|
+
const minted = key('tool');
|
|
566
|
+
const h = routedHarness([cloudInstanceRoute, ...exchangeRoutes([minted]), meRoute()], env);
|
|
567
|
+
expect(await run(['login'], h.io)).toBe(0);
|
|
568
|
+
const origin = new URL(cloudUrl).origin;
|
|
569
|
+
expect(h.wire.map((c) => `${c.method} ${c.origin}${c.path}`)).toEqual([
|
|
570
|
+
`GET ${origin}/api/v1/instance`,
|
|
571
|
+
`GET ${origin}/api/v1/instance`,
|
|
572
|
+
`POST ${HUB}/api/v1/sso/tool-token`,
|
|
573
|
+
`POST ${origin}/api/v1/sso/cli-connect`,
|
|
574
|
+
`GET ${origin}/api/v1/me`
|
|
575
|
+
]);
|
|
576
|
+
const config = loadConfig(env);
|
|
577
|
+
expect(config.activeProfile).toBe('cloud');
|
|
578
|
+
expect(Object.keys(config.profiles)).toEqual(['cloud']);
|
|
579
|
+
expect(config.profiles.cloud?.baseUrl).toBe(origin);
|
|
580
|
+
expect(config.profiles.cloud?.connectKeys?.default?.apiKey).toBe(minted);
|
|
581
|
+
expect(h.out()).toBe(`Signed in as Ada <ada@x.co> in Acme on ${origin}.\n`);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('with an Antasphere login stored: no question, the exchange alone', async () => {
|
|
585
|
+
const env = await hubEnv();
|
|
586
|
+
seedHubLogin(env);
|
|
587
|
+
const h = routedHarness([cloudInstanceRoute, ...exchangeRoutes([key('tool')]), meRoute()], env);
|
|
588
|
+
const asked = scripted(h, []);
|
|
589
|
+
expect(await run(['login', '--api-url', 'http://tool'], h.io)).toBe(0);
|
|
590
|
+
expect(asked).toEqual([]);
|
|
591
|
+
expect(h.wire.map((c) => c.path)).toEqual([
|
|
592
|
+
'/api/v1/instance',
|
|
593
|
+
'/api/v1/instance',
|
|
594
|
+
'/api/v1/sso/tool-token',
|
|
595
|
+
'/api/v1/sso/cli-connect',
|
|
596
|
+
'/api/v1/me'
|
|
597
|
+
]);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('a stored Antasphere login the hub rejects: signs in again, then the exchange again', async () => {
|
|
601
|
+
const env = await hubEnv();
|
|
602
|
+
seedHubLogin(env);
|
|
603
|
+
const fresh = 'ant_freshkey_secretsecretsecret1234';
|
|
604
|
+
const h = routedHarness(
|
|
605
|
+
[
|
|
606
|
+
cloudInstanceRoute,
|
|
607
|
+
...otpRoutes(fresh, ORG),
|
|
608
|
+
...exchangeRoutes([key('tool')], {
|
|
609
|
+
toolToken: (n) =>
|
|
610
|
+
n === 1
|
|
611
|
+
? { status: 401, body: { error: { code: 'unauthenticated', message: 'bad key' } } }
|
|
612
|
+
: undefined
|
|
613
|
+
}),
|
|
614
|
+
meRoute()
|
|
615
|
+
],
|
|
616
|
+
env
|
|
617
|
+
);
|
|
618
|
+
scripted(h, ['123456']);
|
|
619
|
+
expect(await run(['login', '--api-url', 'http://tool', '--email', 'ada@x.co'], h.io)).toBe(0);
|
|
620
|
+
expect(h.err()).toContain('The stored Antasphere login was rejected: signing in again.\n');
|
|
621
|
+
expect(h.wire.map((c) => `${c.method} ${c.origin}${c.path} ${c.auth ?? '-'}`)).toEqual([
|
|
622
|
+
'GET http://tool/api/v1/instance -',
|
|
623
|
+
'GET http://tool/api/v1/instance -',
|
|
624
|
+
`POST ${HUB}/api/v1/sso/tool-token Bearer ${HUB_KEY}`,
|
|
625
|
+
`POST ${HUB}/api/v1/cli/auth/request -`,
|
|
626
|
+
`POST ${HUB}/api/v1/cli/auth/complete -`,
|
|
627
|
+
'GET http://tool/api/v1/instance -',
|
|
628
|
+
`POST ${HUB}/api/v1/sso/tool-token Bearer ${fresh}`,
|
|
629
|
+
'POST http://tool/api/v1/sso/cli-connect -',
|
|
630
|
+
`GET http://tool/api/v1/me Bearer ${key('tool')}`
|
|
631
|
+
]);
|
|
632
|
+
expect(loadCoreConfig(env, HUB_TOOL).profiles.default?.apiKey).toBe(fresh);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('--email naming ANOTHER account than the stored login signs that account in afresh, and the cached key of the old one is revoked and replaced (PRDCT-3032)', async () => {
|
|
636
|
+
const env = await hubEnv();
|
|
637
|
+
seedHubLogin(env);
|
|
638
|
+
const old = key('adakey');
|
|
639
|
+
const minted = key('bobkey');
|
|
640
|
+
saveConfig(env, {
|
|
641
|
+
activeProfile: 'tool',
|
|
642
|
+
profiles: {
|
|
643
|
+
tool: { baseUrl: 'http://tool', connectKeys: { default: { apiKey: old, email: 'ada@x.co' } } }
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
const h = routedHarness(
|
|
647
|
+
[
|
|
648
|
+
cloudInstanceRoute,
|
|
649
|
+
{ method: 'DELETE', path: /\/api\/v1\/cli\/auth\/key$/, reply: () => ({ body: { revoked: true } }) },
|
|
650
|
+
...answeringAs('bob@x.co', [...otpRoutes(BOB_HUB_KEY, ORG), ...exchangeRoutes([minted])]),
|
|
651
|
+
meRoute()
|
|
652
|
+
],
|
|
653
|
+
env
|
|
654
|
+
);
|
|
655
|
+
const asked = scripted(h, ['123456']);
|
|
656
|
+
expect(await run(['login', '--email', 'bob@x.co', '--api-url', 'http://tool'], h.io)).toBe(0);
|
|
657
|
+
expect(asked).toEqual(['Code: ']);
|
|
658
|
+
expect(h.err()).toContain('Replacing the Antasphere login ada@x.co with bob@x.co.\n');
|
|
659
|
+
expect(h.wire.map((c) => `${c.method} ${c.origin}${c.path} ${c.auth ?? '-'}`)).toEqual([
|
|
660
|
+
'GET http://tool/api/v1/instance -',
|
|
661
|
+
`POST ${HUB}/api/v1/cli/auth/request -`,
|
|
662
|
+
`POST ${HUB}/api/v1/cli/auth/complete -`,
|
|
663
|
+
`DELETE http://tool/api/v1/cli/auth/key Bearer ${old}`,
|
|
664
|
+
'GET http://tool/api/v1/instance -',
|
|
665
|
+
`POST ${HUB}/api/v1/sso/tool-token Bearer ${BOB_HUB_KEY}`,
|
|
666
|
+
'POST http://tool/api/v1/sso/cli-connect -',
|
|
667
|
+
`GET http://tool/api/v1/me Bearer ${minted}`
|
|
668
|
+
]);
|
|
669
|
+
expect(h.wire[1]?.body).toEqual({ email: 'bob@x.co' });
|
|
670
|
+
expect(loadCoreConfig(env, HUB_TOOL).profiles.default).toEqual({
|
|
671
|
+
apiKey: BOB_HUB_KEY,
|
|
672
|
+
baseUrl: HUB,
|
|
673
|
+
email: 'bob@x.co',
|
|
674
|
+
workspaceId: ORG
|
|
675
|
+
});
|
|
676
|
+
const tool = loadConfig(env).profiles.tool;
|
|
677
|
+
expect(Object.keys(tool?.connectKeys ?? {})).toEqual(['default']);
|
|
678
|
+
expect(tool?.connectKeys?.default).toMatchObject({ apiKey: minted, email: 'bob@x.co' });
|
|
679
|
+
expect(JSON.stringify(loadConfig(env))).not.toContain(old);
|
|
680
|
+
expect(JSON.stringify(loadCoreConfig(env, HUB_TOOL))).not.toContain(HUB_KEY);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('--email naming the SAME account as the stored login (any case): no question, no new sign-in, the exchange alone', async () => {
|
|
684
|
+
const env = await hubEnv();
|
|
685
|
+
seedHubLogin(env);
|
|
686
|
+
const h = routedHarness(
|
|
687
|
+
[cloudInstanceRoute, ...otpRoutes(BOB_HUB_KEY, ORG), ...exchangeRoutes([key('tool')]), meRoute()],
|
|
688
|
+
env
|
|
689
|
+
);
|
|
690
|
+
const asked = scripted(h, []);
|
|
691
|
+
expect(await run(['login', '--email', 'ADA@X.co', '--api-url', 'http://tool'], h.io)).toBe(0);
|
|
692
|
+
expect(asked).toEqual([]);
|
|
693
|
+
expect(h.err()).not.toContain('Replacing');
|
|
694
|
+
expect(h.wire.map((c) => `${c.method} ${c.origin}${c.path} ${c.auth ?? '-'}`)).toEqual([
|
|
695
|
+
'GET http://tool/api/v1/instance -',
|
|
696
|
+
'GET http://tool/api/v1/instance -',
|
|
697
|
+
`POST ${HUB}/api/v1/sso/tool-token Bearer ${HUB_KEY}`,
|
|
698
|
+
'POST http://tool/api/v1/sso/cli-connect -',
|
|
699
|
+
`GET http://tool/api/v1/me Bearer ${key('tool')}`
|
|
700
|
+
]);
|
|
701
|
+
expect(loadCoreConfig(env, HUB_TOOL).profiles.default?.apiKey).toBe(HUB_KEY);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('--json: via antasphere, the hub profile, the workspaces', async () => {
|
|
705
|
+
const env = await hubEnv();
|
|
706
|
+
seedHubLogin(env);
|
|
707
|
+
const me = meBody({ workspaces: ORG_ROWS });
|
|
708
|
+
const h = routedHarness([cloudInstanceRoute, ...exchangeRoutes([key('tool')]), meRoute(me)], env);
|
|
709
|
+
expect(await run(['login', '--json', '--api-url', 'http://tool'], h.io)).toBe(0);
|
|
710
|
+
expect(JSON.parse(h.out())).toEqual({
|
|
711
|
+
profile: 'tool',
|
|
712
|
+
baseUrl: 'http://tool',
|
|
713
|
+
via: 'antasphere',
|
|
714
|
+
hubProfile: 'default',
|
|
715
|
+
user: me.user,
|
|
716
|
+
workspace: me.workspace,
|
|
717
|
+
workspaces: me.workspaces,
|
|
718
|
+
configPath: configPath(env)
|
|
719
|
+
});
|
|
720
|
+
expect(h.out()).not.toContain(key('tool'));
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('a hub refusal of the person is its own sentence, and exit 3', async () => {
|
|
724
|
+
const env = await hubEnv();
|
|
725
|
+
seedHubLogin(env);
|
|
726
|
+
const h = routedHarness(
|
|
727
|
+
[cloudInstanceRoute, ...exchangeRoutes([key('tool')], { toolToken: () => TOOL_ACCESS_DENIED })],
|
|
728
|
+
env
|
|
729
|
+
);
|
|
730
|
+
expect(await run(['login', '--api-url', 'http://tool'], h.io)).toBe(3);
|
|
731
|
+
expect(h.err()).toBe(`Error: This account has no access to ${displayName} in any of its organizations\n`);
|
|
732
|
+
expect(loadConfig(env).profiles.tool).toBeUndefined();
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
describe('the old sign-in pair is gone', () => {
|
|
737
|
+
it.each([['login-request'], ['login-complete']])('`auth %s` is an unknown command', async (sub) => {
|
|
738
|
+
const h = routedHarness([ossInstanceRoute] as Route[]);
|
|
739
|
+
expect(await run(['auth', sub, '--email', 'ada@x.co'], h.io)).toBe(1);
|
|
740
|
+
expect(h.err()).toContain("unknown command 'auth'");
|
|
741
|
+
expect(h.wire).toEqual([]);
|
|
742
|
+
});
|
|
743
|
+
});
|