@project-ajax/sdk 0.0.50 → 0.0.52
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/dist/cli/api/client.d.ts +0 -8
- package/dist/cli/api/client.d.ts.map +1 -1
- package/dist/cli/api/client.js +4 -31
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.impl.d.ts +4 -9
- package/dist/cli/commands/auth.impl.d.ts.map +1 -1
- package/dist/cli/commands/auth.impl.js +4 -10
- package/dist/cli/commands/auth.impl.test.d.ts +2 -0
- package/dist/cli/commands/auth.impl.test.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +1 -15
- package/dist/cli/commands/bundle.impl.test.d.ts +2 -0
- package/dist/cli/commands/bundle.impl.test.d.ts.map +1 -0
- package/dist/cli/commands/deploy.impl.test.d.ts +2 -0
- package/dist/cli/commands/deploy.impl.test.d.ts.map +1 -0
- package/dist/cli/commands/utils/testing.d.ts +25 -0
- package/dist/cli/commands/utils/testing.d.ts.map +1 -0
- package/dist/cli/commands/utils/testing.js +51 -0
- package/dist/cli/config.d.ts +8 -8
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +56 -19
- package/dist/cli/config.test.d.ts +2 -0
- package/dist/cli/config.test.d.ts.map +1 -0
- package/dist/cli/flags.d.ts +5 -0
- package/dist/cli/flags.d.ts.map +1 -1
- package/dist/cli/flags.js +25 -0
- package/dist/cli/handler.d.ts.map +1 -1
- package/dist/cli/handler.js +3 -5
- package/package.json +2 -2
- package/src/capabilities/tool.test.ts +51 -1
- package/src/cli/api/client.ts +3 -44
- package/src/cli/commands/.cursor/rules/testing-commands.mdc +212 -0
- package/src/cli/commands/auth.impl.test.ts +206 -0
- package/src/cli/commands/auth.impl.ts +5 -17
- package/src/cli/commands/auth.ts +2 -15
- package/src/cli/commands/bundle.impl.test.ts +137 -0
- package/src/cli/commands/deploy.impl.test.ts +239 -0
- package/src/cli/commands/utils/testing.ts +60 -0
- package/src/cli/config.test.ts +114 -0
- package/src/cli/config.ts +71 -29
- package/src/cli/flags.ts +29 -0
- package/src/cli/handler.ts +2 -5
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
type Mock,
|
|
8
|
+
vi,
|
|
9
|
+
} from "vitest";
|
|
2
10
|
import {
|
|
3
11
|
InvalidToolInputError,
|
|
4
12
|
InvalidToolOutputError,
|
|
@@ -7,6 +15,18 @@ import {
|
|
|
7
15
|
} from "./tool.js";
|
|
8
16
|
|
|
9
17
|
describe("tool", () => {
|
|
18
|
+
let stdoutSpy: Mock<typeof process.stdout.write>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
stdoutSpy = vi
|
|
22
|
+
.spyOn(process.stdout, "write")
|
|
23
|
+
.mockImplementation(() => true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
10
30
|
it("sync execution", async () => {
|
|
11
31
|
const myTool = tool<{ name: string }, string>({
|
|
12
32
|
title: "Say Hello",
|
|
@@ -30,6 +50,10 @@ describe("tool", () => {
|
|
|
30
50
|
if (result._tag === "success") {
|
|
31
51
|
expect(result.value).toBe("Hello, Alice!");
|
|
32
52
|
}
|
|
53
|
+
|
|
54
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
55
|
+
`\n<output>"Hello, Alice!"</output>\n`,
|
|
56
|
+
);
|
|
33
57
|
});
|
|
34
58
|
|
|
35
59
|
it("async execution", async () => {
|
|
@@ -57,6 +81,10 @@ describe("tool", () => {
|
|
|
57
81
|
if (result._tag === "success") {
|
|
58
82
|
expect(result.value).toEqual({ data: "Data for ID 42" });
|
|
59
83
|
}
|
|
84
|
+
|
|
85
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
86
|
+
`\n<output>{"data":"Data for ID 42"}</output>\n`,
|
|
87
|
+
);
|
|
60
88
|
});
|
|
61
89
|
|
|
62
90
|
it("execution error", async () => {
|
|
@@ -81,6 +109,10 @@ describe("tool", () => {
|
|
|
81
109
|
expect(result.error).toBeInstanceOf(ToolExecutionError);
|
|
82
110
|
expect(result.error.message).toBe("Something went wrong");
|
|
83
111
|
}
|
|
112
|
+
|
|
113
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
114
|
+
`\n<output>{"_tag":"error","error":{"name":"ToolExecutionError","message":"Something went wrong"}}</output>\n`,
|
|
115
|
+
);
|
|
84
116
|
});
|
|
85
117
|
|
|
86
118
|
it("invalid input", async () => {
|
|
@@ -107,6 +139,12 @@ describe("tool", () => {
|
|
|
107
139
|
expect(result.error).toBeInstanceOf(InvalidToolInputError);
|
|
108
140
|
expect(result.error.message).toContain("name");
|
|
109
141
|
}
|
|
142
|
+
|
|
143
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
144
|
+
expect.stringContaining(
|
|
145
|
+
`\n<output>{"_tag":"error","error":{"name":"InvalidToolInputError"`,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
110
148
|
});
|
|
111
149
|
|
|
112
150
|
it("invalid output", async () => {
|
|
@@ -140,6 +178,12 @@ describe("tool", () => {
|
|
|
140
178
|
expect(result.error).toBeInstanceOf(InvalidToolOutputError);
|
|
141
179
|
expect(result.error.message).toContain("result");
|
|
142
180
|
}
|
|
181
|
+
|
|
182
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
183
|
+
expect.stringContaining(
|
|
184
|
+
`\n<output>{"_tag":"error","error":{"name":"InvalidToolOutputError"`,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
143
187
|
});
|
|
144
188
|
|
|
145
189
|
it("invalid output with custom output schema", async () => {
|
|
@@ -181,5 +225,11 @@ describe("tool", () => {
|
|
|
181
225
|
expect(result.error).toBeInstanceOf(InvalidToolOutputError);
|
|
182
226
|
expect(result.error.message).toContain("message");
|
|
183
227
|
}
|
|
228
|
+
|
|
229
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
230
|
+
expect.stringContaining(
|
|
231
|
+
`\n<output>{"_tag":"error","error":{"name":"InvalidToolOutputError"`,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
184
234
|
});
|
|
185
235
|
});
|
package/src/cli/api/client.ts
CHANGED
|
@@ -29,65 +29,24 @@ export interface ApiError {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
* Get the base URL for the given environment
|
|
34
|
-
*/
|
|
35
|
-
export function baseUrl({
|
|
36
|
-
environment,
|
|
37
|
-
override,
|
|
38
|
-
}: {
|
|
39
|
-
environment: Environment;
|
|
40
|
-
override?: string | undefined;
|
|
41
|
-
}): string {
|
|
42
|
-
if (override) {
|
|
43
|
-
return override;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
switch (environment) {
|
|
47
|
-
case "local":
|
|
48
|
-
return "http://localhost:3000";
|
|
49
|
-
case "staging":
|
|
50
|
-
return "https://staging.notion.so";
|
|
51
|
-
case "dev":
|
|
52
|
-
return "https://dev.notion.so";
|
|
53
|
-
case "prod":
|
|
54
|
-
return "https://www.notion.so";
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Get the base API URL for the given environment
|
|
60
|
-
*/
|
|
61
|
-
function baseApiUrl({
|
|
62
|
-
environment,
|
|
63
|
-
override,
|
|
64
|
-
}: {
|
|
65
|
-
environment: Environment;
|
|
66
|
-
override?: string | undefined;
|
|
67
|
-
}): string {
|
|
68
|
-
return `${baseUrl({ environment, override })}/api/v3`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
32
|
/**
|
|
72
33
|
* API client for making authenticated requests to Workers endpoints
|
|
73
34
|
*/
|
|
74
35
|
export class ApiClient {
|
|
75
36
|
readonly #token: string;
|
|
76
|
-
readonly #env: Environment;
|
|
77
37
|
readonly #baseUrl: string | undefined;
|
|
78
38
|
readonly #cellId: string;
|
|
79
39
|
readonly #writer: Writer;
|
|
80
40
|
|
|
81
41
|
constructor(config: ApiClientConfig) {
|
|
82
42
|
this.#token = config.token;
|
|
83
|
-
this.#env = config.environment;
|
|
84
43
|
this.#baseUrl = config.baseUrl;
|
|
85
44
|
this.#cellId = config.cellId;
|
|
86
45
|
this.#writer = config.writer;
|
|
87
46
|
}
|
|
88
47
|
|
|
89
|
-
|
|
90
|
-
return
|
|
48
|
+
#baseApiUrl() {
|
|
49
|
+
return `${this.#baseUrl}/api/v3`;
|
|
91
50
|
}
|
|
92
51
|
|
|
93
52
|
/**
|
|
@@ -113,7 +72,7 @@ export class ApiClient {
|
|
|
113
72
|
throw new Error("Endpoint must start with a slash (/)");
|
|
114
73
|
}
|
|
115
74
|
|
|
116
|
-
const url = `${this
|
|
75
|
+
const url = `${this.#baseApiUrl()}${endpoint}`;
|
|
117
76
|
|
|
118
77
|
this.#writer.debug(`Fetching ${url}`);
|
|
119
78
|
this.#writer.debug(
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Testing patterns for CLI command implementations
|
|
3
|
+
globs:
|
|
4
|
+
- "**/*.impl.test.ts"
|
|
5
|
+
alwaysApply: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Testing CLI Commands
|
|
9
|
+
|
|
10
|
+
## Understanding buildHandler
|
|
11
|
+
|
|
12
|
+
Commands are wrapped with `buildHandler` which:
|
|
13
|
+
1. Returns a function that accepts `(flags, ...args)`
|
|
14
|
+
2. Internally loads config using `Config.load()`
|
|
15
|
+
3. Requires a `this` context with `process`, `fs`, `os`, `path`, and `writer`
|
|
16
|
+
|
|
17
|
+
## Test Structure Pattern
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
describe("commandName", () => {
|
|
21
|
+
let stderrSpy: Mock<typeof process.stderr.write>; // or stdoutSpy
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
stderrSpy = vi
|
|
25
|
+
.spyOn(process.stderr, "write")
|
|
26
|
+
.mockImplementation(() => true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does something", async () => {
|
|
30
|
+
// 1. Create a mock config with the desired state
|
|
31
|
+
const mockConfig = await createTestConfig({
|
|
32
|
+
token: "test-token",
|
|
33
|
+
workerId: null,
|
|
34
|
+
environment: "local",
|
|
35
|
+
baseUrl: "http://localhost:3000",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 2. Spy on Config.load to return your mock config
|
|
39
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
40
|
+
|
|
41
|
+
// 3. Spy on config methods to verify they're called
|
|
42
|
+
const setTokenSpy = vi.spyOn(mockConfig, "setToken");
|
|
43
|
+
|
|
44
|
+
// 4. Create a base context
|
|
45
|
+
const context = createBaseContext();
|
|
46
|
+
|
|
47
|
+
// 5. Call the handler with .call(context, flags, ...args)
|
|
48
|
+
await commandHandler.call(context, baseFlags, "arg1", "arg2");
|
|
49
|
+
|
|
50
|
+
// 6. Assert on spies and output
|
|
51
|
+
expect(setTokenSpy).toHaveBeenCalledWith("expected-value");
|
|
52
|
+
expect(stderrSpy).toHaveBeenCalledWith(
|
|
53
|
+
expect.stringContaining("Expected message"),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Key Testing Patterns
|
|
60
|
+
|
|
61
|
+
### 1. Mocking Config.load
|
|
62
|
+
|
|
63
|
+
Always mock `Config.load` to return your test config. The handler loads config internally, so you can't pass it directly.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const mockConfig = await createTestConfig({
|
|
67
|
+
token: null,
|
|
68
|
+
workerId: null,
|
|
69
|
+
environment: "local",
|
|
70
|
+
baseUrl: "http://localhost:3000",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Creating Context
|
|
77
|
+
|
|
78
|
+
Handlers expect a context with specific properties:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
function createBaseContext() {
|
|
82
|
+
return {
|
|
83
|
+
writer: new Writer({ debugEnabled: false }),
|
|
84
|
+
process,
|
|
85
|
+
fs,
|
|
86
|
+
os,
|
|
87
|
+
path,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Calling Handlers
|
|
93
|
+
|
|
94
|
+
Use `.call()` to provide the context:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
await handler.call(context, flags, ...args);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Don't** call the handler directly without context - it will fail because `this.process` will be undefined.
|
|
101
|
+
|
|
102
|
+
### 4. Verifying Config Updates
|
|
103
|
+
|
|
104
|
+
Spy on config methods to verify they were called, rather than checking state directly:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const setTokenSpy = vi.spyOn(mockConfig, "setToken");
|
|
108
|
+
await handler.call(context, flags);
|
|
109
|
+
expect(setTokenSpy).toHaveBeenCalledWith(expectedValue);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Config methods like `setToken()`, `setEnvironment()`, and `setWorkerId()` write to the file asynchronously, so checking the config state immediately won't work.
|
|
113
|
+
|
|
114
|
+
### 5. Mocking External Dependencies
|
|
115
|
+
|
|
116
|
+
Mock external modules before importing the implementation:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
vi.mock("../utils/openUrl.js", () => ({
|
|
120
|
+
openNotionUrl: vi.fn().mockResolvedValue(undefined),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
import { commandHandler } from "./command.impl.js";
|
|
124
|
+
import { openNotionUrl } from "../utils/openUrl.js";
|
|
125
|
+
|
|
126
|
+
// In your test:
|
|
127
|
+
expect(openNotionUrl).toHaveBeenCalledWith("local", "http://...");
|
|
128
|
+
|
|
129
|
+
// For one-time failures:
|
|
130
|
+
vi.mocked(openNotionUrl).mockRejectedValueOnce(new Error("Failed"));
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 6. Capturing stdout/stderr
|
|
134
|
+
|
|
135
|
+
When testing output, spy on the write methods and check the content:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
let stderrSpy: Mock<typeof process.stderr.write>;
|
|
139
|
+
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
stderrSpy = vi
|
|
142
|
+
.spyOn(process.stderr, "write")
|
|
143
|
+
.mockImplementation(() => true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// For single messages:
|
|
147
|
+
expect(stderrSpy).toHaveBeenCalledWith(
|
|
148
|
+
expect.stringContaining("Expected message"),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// For multiple messages (concatenate all calls):
|
|
152
|
+
const allCalls = stderrSpy.mock.calls.map((call) => call[0]).join("");
|
|
153
|
+
expect(allCalls).toContain("Expected message");
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Helper Functions
|
|
157
|
+
|
|
158
|
+
Place helper functions at the bottom of test files:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Test helpers
|
|
162
|
+
|
|
163
|
+
type ConfigFileContents = {
|
|
164
|
+
token: string | null;
|
|
165
|
+
workerId: string | null;
|
|
166
|
+
environment: Environment;
|
|
167
|
+
baseUrl: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const tmpDirectories: string[] = [];
|
|
171
|
+
|
|
172
|
+
const baseFlags: GlobalFlags = {
|
|
173
|
+
debug: false,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
async function createConfigFile(contents: ConfigFileContents) {
|
|
177
|
+
const dir = await mkdtemp(path.join(tmpdir(), "test-prefix-"));
|
|
178
|
+
tmpDirectories.push(dir);
|
|
179
|
+
const configFilePath = path.join(dir, "config.json");
|
|
180
|
+
await writeFile(configFilePath, JSON.stringify(contents, null, 2), "utf-8");
|
|
181
|
+
return configFilePath;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function createTestConfig(
|
|
185
|
+
configFileContents: ConfigFileContents,
|
|
186
|
+
): Promise<Config> {
|
|
187
|
+
const configFilePath = await createConfigFile(configFileContents);
|
|
188
|
+
return Config.load({
|
|
189
|
+
configFilePath,
|
|
190
|
+
processEnv: {} as NodeJS.ProcessEnv,
|
|
191
|
+
flags: { debug: false },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createBaseContext() {
|
|
196
|
+
return {
|
|
197
|
+
writer: new Writer({ debugEnabled: false }),
|
|
198
|
+
process,
|
|
199
|
+
fs,
|
|
200
|
+
os,
|
|
201
|
+
path,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Common Mistakes to Avoid
|
|
207
|
+
|
|
208
|
+
1. **Don't call handlers directly without context**: Always use `.call(context, ...)`
|
|
209
|
+
2. **Don't check config state directly**: Spy on the setter methods instead
|
|
210
|
+
3. **Don't forget to mock Config.load**: The handler loads config internally
|
|
211
|
+
4. **Don't forget to create unique temp directories**: Use `mkdtemp` with unique prefixes
|
|
212
|
+
5. **Don't forget to clean up temp files**: Use `afterEach` to remove temp directories
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
it,
|
|
7
|
+
type Mock,
|
|
8
|
+
vi,
|
|
9
|
+
} from "vitest";
|
|
10
|
+
import { Config } from "../config.js";
|
|
11
|
+
import {
|
|
12
|
+
baseFlags,
|
|
13
|
+
cleanupTmpDirectories,
|
|
14
|
+
createBaseContext,
|
|
15
|
+
createTestConfig,
|
|
16
|
+
} from "./utils/testing.js";
|
|
17
|
+
|
|
18
|
+
// Mock the openUrl module before importing the implementation
|
|
19
|
+
vi.mock("../utils/openUrl.js", () => ({
|
|
20
|
+
openNotionUrl: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { openNotionUrl } from "../utils/openUrl.js";
|
|
24
|
+
import { login, logout, show } from "./auth.impl.js";
|
|
25
|
+
|
|
26
|
+
afterEach(cleanupTmpDirectories);
|
|
27
|
+
|
|
28
|
+
describe("login", () => {
|
|
29
|
+
let stderrSpy: Mock<typeof process.stderr.write>;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
stderrSpy = vi
|
|
33
|
+
.spyOn(process.stderr, "write")
|
|
34
|
+
.mockImplementation(() => true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("opens browser and displays instructions when no token provided", async () => {
|
|
38
|
+
const mockConfig = await createTestConfig({
|
|
39
|
+
token: null,
|
|
40
|
+
workerId: null,
|
|
41
|
+
environment: "local",
|
|
42
|
+
baseUrl: "http://localhost:3000",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
46
|
+
|
|
47
|
+
const context = createBaseContext();
|
|
48
|
+
|
|
49
|
+
await login.call(context, baseFlags);
|
|
50
|
+
|
|
51
|
+
expect(openNotionUrl).toHaveBeenCalledWith(
|
|
52
|
+
"local",
|
|
53
|
+
"http://localhost:3000/__workers__?createToken=true",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const allCalls = stderrSpy.mock.calls.map((call) => call[0]).join("");
|
|
57
|
+
expect(allCalls).toContain("Opening browser to create a token...");
|
|
58
|
+
expect(allCalls).toContain(
|
|
59
|
+
"http://localhost:3000/__workers__?createToken=true",
|
|
60
|
+
);
|
|
61
|
+
expect(allCalls).toContain("After creating a token, run:");
|
|
62
|
+
expect(allCalls).toContain("npx workers auth login <token>");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("shows error message when browser fails to open", async () => {
|
|
66
|
+
const mockConfig = await createTestConfig({
|
|
67
|
+
token: null,
|
|
68
|
+
workerId: null,
|
|
69
|
+
environment: "local",
|
|
70
|
+
baseUrl: "http://localhost:3000",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
74
|
+
vi.mocked(openNotionUrl).mockRejectedValueOnce(new Error("Failed to open"));
|
|
75
|
+
|
|
76
|
+
const context = createBaseContext();
|
|
77
|
+
|
|
78
|
+
await login.call(context, baseFlags);
|
|
79
|
+
|
|
80
|
+
const allCalls = stderrSpy.mock.calls.map((call) => call[0]).join("");
|
|
81
|
+
expect(allCalls).toContain("Failed to open browser automatically");
|
|
82
|
+
expect(allCalls).toContain(
|
|
83
|
+
"http://localhost:3000/__workers__?createToken=true",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("saves token and clears workerId when token is provided", async () => {
|
|
88
|
+
const mockConfig = await createTestConfig({
|
|
89
|
+
token: null,
|
|
90
|
+
workerId: null,
|
|
91
|
+
environment: "local",
|
|
92
|
+
baseUrl: "http://localhost:3000",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Spy on Config.load to return our mock config
|
|
96
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
97
|
+
|
|
98
|
+
const testToken =
|
|
99
|
+
"1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig";
|
|
100
|
+
|
|
101
|
+
const setEnvironmentSpy = vi.spyOn(mockConfig, "setEnvironment");
|
|
102
|
+
const setTokenSpy = vi.spyOn(mockConfig, "setToken");
|
|
103
|
+
const setWorkerIdSpy = vi.spyOn(mockConfig, "setWorkerId");
|
|
104
|
+
|
|
105
|
+
const context = createBaseContext();
|
|
106
|
+
|
|
107
|
+
await login.call(context, baseFlags, testToken);
|
|
108
|
+
|
|
109
|
+
expect(setEnvironmentSpy).toHaveBeenCalledWith("local");
|
|
110
|
+
expect(setTokenSpy).toHaveBeenCalledWith(testToken);
|
|
111
|
+
expect(setWorkerIdSpy).toHaveBeenCalledWith(null);
|
|
112
|
+
expect(stderrSpy).toHaveBeenCalledWith(
|
|
113
|
+
expect.stringContaining("Successfully logged in!"),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("saves environment from config when logging in", async () => {
|
|
118
|
+
const mockConfig = await createTestConfig({
|
|
119
|
+
token: null,
|
|
120
|
+
workerId: null,
|
|
121
|
+
environment: "prod",
|
|
122
|
+
baseUrl: "https://www.notion.so",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
126
|
+
|
|
127
|
+
const testToken =
|
|
128
|
+
"1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig";
|
|
129
|
+
|
|
130
|
+
const setEnvironmentSpy = vi.spyOn(mockConfig, "setEnvironment");
|
|
131
|
+
|
|
132
|
+
const context = createBaseContext();
|
|
133
|
+
|
|
134
|
+
await login.call(context, baseFlags, testToken);
|
|
135
|
+
|
|
136
|
+
expect(setEnvironmentSpy).toHaveBeenCalledWith("prod");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("show", () => {
|
|
141
|
+
let stdoutSpy: Mock<typeof process.stdout.write>;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
stdoutSpy = vi
|
|
145
|
+
.spyOn(process.stdout, "write")
|
|
146
|
+
.mockImplementation(() => true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("writes token to stdout when token exists", async () => {
|
|
150
|
+
const testToken =
|
|
151
|
+
"1.2.eyJzcGFjZUlkIjoic3BhY2UxIiwidXNlcklkIjoidXNlcjEiLCJjZWxsSWQiOiJjZWxsMSJ9.sig";
|
|
152
|
+
|
|
153
|
+
const mockConfig = await createTestConfig({
|
|
154
|
+
token: testToken,
|
|
155
|
+
workerId: "worker-1",
|
|
156
|
+
environment: "local",
|
|
157
|
+
baseUrl: "http://localhost:3000",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
161
|
+
|
|
162
|
+
const context = createBaseContext();
|
|
163
|
+
|
|
164
|
+
await show.call(context, baseFlags);
|
|
165
|
+
|
|
166
|
+
expect(stdoutSpy).toHaveBeenCalledWith(`${testToken}\n`);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("writes empty string to stdout when no token exists", async () => {
|
|
170
|
+
const mockConfig = await createTestConfig({
|
|
171
|
+
token: null,
|
|
172
|
+
workerId: null,
|
|
173
|
+
environment: "local",
|
|
174
|
+
baseUrl: "http://localhost:3000",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
178
|
+
|
|
179
|
+
const context = createBaseContext();
|
|
180
|
+
|
|
181
|
+
await show.call(context, baseFlags);
|
|
182
|
+
|
|
183
|
+
expect(stdoutSpy).toHaveBeenCalledWith("\n");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("logout", () => {
|
|
188
|
+
it("calls setToken with null", async () => {
|
|
189
|
+
const mockConfig = await createTestConfig({
|
|
190
|
+
token: "existing-token",
|
|
191
|
+
workerId: "worker-1",
|
|
192
|
+
environment: "local",
|
|
193
|
+
baseUrl: "http://localhost:3000",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
vi.spyOn(Config, "load").mockResolvedValue(mockConfig);
|
|
197
|
+
|
|
198
|
+
const setTokenSpy = vi.spyOn(mockConfig, "setToken");
|
|
199
|
+
|
|
200
|
+
const context = createBaseContext();
|
|
201
|
+
|
|
202
|
+
await logout.call(context, baseFlags);
|
|
203
|
+
|
|
204
|
+
expect(setTokenSpy).toHaveBeenCalledWith(null);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Environment } from "../config.js";
|
|
1
|
+
import type { GlobalFlags } from "../flags.js";
|
|
3
2
|
import { buildHandler, type HandlerContext } from "../handler.js";
|
|
4
3
|
import { openNotionUrl } from "../utils/openUrl.js";
|
|
5
4
|
|
|
6
|
-
interface LoginFlags {
|
|
7
|
-
env?: Environment;
|
|
8
|
-
"base-url"?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
export const login = buildHandler(async function (
|
|
12
6
|
this: HandlerContext,
|
|
13
|
-
|
|
7
|
+
_: GlobalFlags,
|
|
14
8
|
token?: string,
|
|
15
9
|
) {
|
|
16
|
-
const environment =
|
|
17
|
-
|
|
18
|
-
if (flags["base-url"]) {
|
|
19
|
-
await this.config.setBaseUrl(flags["base-url"]);
|
|
20
|
-
}
|
|
10
|
+
const environment = this.config.environment;
|
|
21
11
|
|
|
22
12
|
// If no token provided, open the browser to get one
|
|
23
13
|
if (!token) {
|
|
24
|
-
const url = `${baseUrl
|
|
25
|
-
environment,
|
|
26
|
-
override: this.config.baseUrl,
|
|
27
|
-
})}/__workers__?createToken=true`;
|
|
14
|
+
const url = `${this.config.baseUrl}/__workers__?createToken=true`;
|
|
28
15
|
|
|
29
16
|
this.writer.writeErr(`Opening browser to create a token...\n${url}\n`);
|
|
30
17
|
this.writer.writeErr("After creating a token, run:");
|
|
@@ -43,6 +30,7 @@ export const login = buildHandler(async function (
|
|
|
43
30
|
|
|
44
31
|
await this.config.setEnvironment(environment);
|
|
45
32
|
await this.config.setToken(token);
|
|
33
|
+
await this.config.setWorkerId(null);
|
|
46
34
|
|
|
47
35
|
this.writer.writeErr("Successfully logged in!");
|
|
48
36
|
});
|
package/src/cli/commands/auth.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { buildCommand, buildRouteMap } from "@stricli/core";
|
|
2
|
-
import { Environments } from "../config.js";
|
|
3
2
|
import { globalFlags } from "../flags.js";
|
|
4
3
|
|
|
5
4
|
export const authCommands = buildRouteMap({
|
|
@@ -9,7 +8,8 @@ export const authCommands = buildRouteMap({
|
|
|
9
8
|
routes: {
|
|
10
9
|
login: buildCommand({
|
|
11
10
|
docs: {
|
|
12
|
-
brief:
|
|
11
|
+
brief:
|
|
12
|
+
"Login to the Project Ajax platform using a Workers API token; will clear existing token, environment, and worker ID",
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
parameters: {
|
|
@@ -26,19 +26,6 @@ export const authCommands = buildRouteMap({
|
|
|
26
26
|
},
|
|
27
27
|
|
|
28
28
|
flags: {
|
|
29
|
-
env: {
|
|
30
|
-
kind: "enum",
|
|
31
|
-
values: Environments,
|
|
32
|
-
brief: "The environment to use for the command",
|
|
33
|
-
optional: true,
|
|
34
|
-
},
|
|
35
|
-
"base-url": {
|
|
36
|
-
kind: "parsed",
|
|
37
|
-
parse: String,
|
|
38
|
-
brief: "The base URL to use for all API requests",
|
|
39
|
-
optional: true,
|
|
40
|
-
hidden: true,
|
|
41
|
-
},
|
|
42
29
|
...globalFlags,
|
|
43
30
|
},
|
|
44
31
|
},
|