claude-remote-cli 3.10.0 → 3.11.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/dist/frontend/assets/index-B7wmLeyf.js +52 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server/branch-linker.js +3 -5
- package/dist/server/index.js +5 -20
- package/dist/server/integration-jira.js +103 -108
- package/dist/server/ticket-transitions.js +17 -129
- package/dist/test/integration-jira.test.js +133 -214
- package/dist/test/ticket-transitions.test.js +52 -257
- package/package.json +1 -1
- package/dist/frontend/assets/index-Dgf6cKGu.js +0 -52
- package/dist/server/integration-linear.js +0 -176
- package/dist/test/integration-linear.test.js +0 -293
|
@@ -1,59 +1,60 @@
|
|
|
1
|
-
import { test,
|
|
1
|
+
import { test, after } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import express from 'express';
|
|
4
4
|
import { createIntegrationJiraRouter } from '../server/integration-jira.js';
|
|
5
|
-
// ───
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// ─── Mock stdout constants ────────────────────────────────────────────────────
|
|
6
|
+
const AUTH_STATUS_STDOUT = `✓ Authenticated
|
|
7
|
+
Site: fake-jira.atlassian.net
|
|
8
|
+
Email: test@example.com
|
|
9
|
+
Authentication Type: oauth
|
|
10
|
+
`;
|
|
9
11
|
// ─── Globals ──────────────────────────────────────────────────────────────────
|
|
10
12
|
let server;
|
|
11
13
|
let baseUrl;
|
|
12
|
-
/**
|
|
13
|
-
* The real Node.js fetch, saved before any test replaces globalThis.fetch.
|
|
14
|
-
* All HTTP calls from the test itself to the local Express server must use this.
|
|
15
|
-
*/
|
|
16
|
-
let realFetch;
|
|
17
14
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
15
|
/**
|
|
19
|
-
* Builds a minimal
|
|
16
|
+
* Builds a minimal AcliWorkItem for use in mock stdout payloads.
|
|
20
17
|
*/
|
|
21
|
-
function
|
|
22
|
-
const { key = 'TEST-1', summary = 'Test issue',
|
|
18
|
+
function makeAcliWorkItem(overrides) {
|
|
19
|
+
const { key = 'TEST-1', summary = 'Test issue', statusId = '3', statusName = 'In Progress', priorityName = 'Medium', assigneeDisplayName = 'Jane Doe', } = overrides;
|
|
23
20
|
return {
|
|
24
21
|
key,
|
|
25
|
-
self: `${FAKE_BASE_URL}/rest/api/3/issue/${key}`,
|
|
26
22
|
fields: {
|
|
27
23
|
summary,
|
|
28
|
-
status: { name:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
customfield_10020: sprint !== null ? [{ name: sprint }] : null,
|
|
32
|
-
assignee: assignee !== null ? { displayName: assignee } : null,
|
|
33
|
-
updated,
|
|
24
|
+
status: { id: statusId, name: statusName },
|
|
25
|
+
...(priorityName !== null ? { priority: { name: priorityName } } : { priority: null }),
|
|
26
|
+
...(assigneeDisplayName !== null ? { assignee: { displayName: assigneeDisplayName } } : { assignee: null }),
|
|
34
27
|
},
|
|
35
28
|
};
|
|
36
29
|
}
|
|
37
30
|
/**
|
|
38
|
-
*
|
|
31
|
+
* Creates a mock execAsync that routes calls based on the acli subcommand args:
|
|
32
|
+
* - `acli jira auth status` → returns AUTH_STATUS_STDOUT (or throws)
|
|
33
|
+
* - `acli jira workitem search` → returns the provided items as JSON stdout (or throws)
|
|
39
34
|
*/
|
|
40
|
-
function
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
function makeMockExec(opts) {
|
|
36
|
+
return async (_cmd, args) => {
|
|
37
|
+
const argv = args;
|
|
38
|
+
if (argv.includes('auth') && argv.includes('status')) {
|
|
39
|
+
if (opts.authError)
|
|
40
|
+
throw opts.authError;
|
|
41
|
+
return { stdout: AUTH_STATUS_STDOUT, stderr: '' };
|
|
42
|
+
}
|
|
43
|
+
if (argv.includes('workitem') && argv.includes('search')) {
|
|
44
|
+
if (opts.searchError)
|
|
45
|
+
throw opts.searchError;
|
|
46
|
+
return { stdout: JSON.stringify(opts.searchItems ?? []), stderr: '' };
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Unexpected exec call: acli ${argv.join(' ')}`);
|
|
49
|
+
};
|
|
45
50
|
}
|
|
46
51
|
// ─── Server lifecycle ─────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
* Starts a fresh Express server with a new Jira router instance (and therefore
|
|
49
|
-
* a fresh in-memory cache). Call this AFTER setting globalThis.fetch so the
|
|
50
|
-
* router uses the mock when handling requests.
|
|
51
|
-
*/
|
|
52
|
-
function startServer() {
|
|
52
|
+
function startServer(execAsyncFn) {
|
|
53
53
|
return new Promise((resolve) => {
|
|
54
54
|
const app = express();
|
|
55
55
|
app.use(express.json());
|
|
56
|
-
|
|
56
|
+
const deps = { configPath: '', execAsync: execAsyncFn };
|
|
57
|
+
app.use('/integration-jira', createIntegrationJiraRouter(deps));
|
|
57
58
|
server = app.listen(0, '127.0.0.1', () => {
|
|
58
59
|
const addr = server.address();
|
|
59
60
|
if (typeof addr === 'object' && addr) {
|
|
@@ -71,88 +72,34 @@ function stopServer() {
|
|
|
71
72
|
resolve();
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
|
-
// ───
|
|
75
|
-
function setEnvVars() {
|
|
76
|
-
process.env.JIRA_API_TOKEN = FAKE_TOKEN;
|
|
77
|
-
process.env.JIRA_EMAIL = FAKE_EMAIL;
|
|
78
|
-
process.env.JIRA_BASE_URL = FAKE_BASE_URL;
|
|
79
|
-
}
|
|
80
|
-
function clearEnvVars() {
|
|
81
|
-
delete process.env.JIRA_API_TOKEN;
|
|
82
|
-
delete process.env.JIRA_EMAIL;
|
|
83
|
-
delete process.env.JIRA_BASE_URL;
|
|
84
|
-
}
|
|
85
|
-
// ─── Suite setup / teardown ───────────────────────────────────────────────────
|
|
86
|
-
before(() => {
|
|
87
|
-
realFetch = globalThis.fetch;
|
|
88
|
-
clearEnvVars();
|
|
89
|
-
});
|
|
75
|
+
// ─── Suite teardown ───────────────────────────────────────────────────────────
|
|
90
76
|
after(async () => {
|
|
91
|
-
globalThis.fetch = realFetch;
|
|
92
|
-
clearEnvVars();
|
|
93
77
|
await stopServer();
|
|
94
78
|
});
|
|
95
79
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
96
|
-
test('GET /
|
|
97
|
-
setEnvVars();
|
|
98
|
-
globalThis.fetch = realFetch;
|
|
99
|
-
await stopServer();
|
|
100
|
-
await startServer();
|
|
101
|
-
const res = await realFetch(`${baseUrl}/integration-jira/configured`);
|
|
102
|
-
assert.equal(res.status, 200);
|
|
103
|
-
const body = (await res.json());
|
|
104
|
-
assert.equal(body.configured, true);
|
|
105
|
-
});
|
|
106
|
-
test('GET /configured returns false when any env var is missing', async () => {
|
|
107
|
-
clearEnvVars();
|
|
108
|
-
// Set only two of the three required vars; JIRA_BASE_URL intentionally omitted
|
|
109
|
-
process.env.JIRA_API_TOKEN = FAKE_TOKEN;
|
|
110
|
-
process.env.JIRA_EMAIL = FAKE_EMAIL;
|
|
111
|
-
globalThis.fetch = realFetch;
|
|
80
|
+
test('GET /issues returns mapped JiraIssue[] from mocked acli JSON output', async () => {
|
|
112
81
|
await stopServer();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
makeJiraApiIssue({
|
|
135
|
-
key: 'PROJ-10',
|
|
136
|
-
summary: 'Update docs',
|
|
137
|
-
status: 'To Do',
|
|
138
|
-
priority: null,
|
|
139
|
-
storyPoints: null,
|
|
140
|
-
sprint: null,
|
|
141
|
-
assignee: null,
|
|
142
|
-
updated: '2026-03-20T08:00:00.000+0000',
|
|
143
|
-
}),
|
|
144
|
-
],
|
|
145
|
-
};
|
|
146
|
-
globalThis.fetch = async (input) => {
|
|
147
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
148
|
-
if (url.includes('/rest/api/3/search')) {
|
|
149
|
-
return makeJsonResponse(mockSearchPayload);
|
|
150
|
-
}
|
|
151
|
-
return makeJsonResponse({ error: 'unexpected url' }, 500);
|
|
152
|
-
};
|
|
153
|
-
await stopServer();
|
|
154
|
-
await startServer();
|
|
155
|
-
const res = await realFetch(`${baseUrl}/integration-jira/issues`);
|
|
82
|
+
const searchItems = [
|
|
83
|
+
makeAcliWorkItem({
|
|
84
|
+
key: 'PROJ-42',
|
|
85
|
+
summary: 'Fix the login bug',
|
|
86
|
+
statusId: '3',
|
|
87
|
+
statusName: 'In Progress',
|
|
88
|
+
priorityName: 'High',
|
|
89
|
+
assigneeDisplayName: 'Alice',
|
|
90
|
+
}),
|
|
91
|
+
makeAcliWorkItem({
|
|
92
|
+
key: 'PROJ-10',
|
|
93
|
+
summary: 'Update docs',
|
|
94
|
+
statusId: '1',
|
|
95
|
+
statusName: 'To Do',
|
|
96
|
+
priorityName: null,
|
|
97
|
+
assigneeDisplayName: null,
|
|
98
|
+
}),
|
|
99
|
+
];
|
|
100
|
+
const exec = makeMockExec({ searchItems });
|
|
101
|
+
await startServer(exec);
|
|
102
|
+
const res = await fetch(`${baseUrl}/integration-jira/issues`);
|
|
156
103
|
assert.equal(res.status, 200);
|
|
157
104
|
const data = (await res.json());
|
|
158
105
|
assert.equal(data.error, undefined, `Unexpected error: ${data.error}`);
|
|
@@ -162,91 +109,83 @@ test('GET /issues returns mapped JiraIssue[] from mocked Jira search response',
|
|
|
162
109
|
assert.equal(issue42.title, 'Fix the login bug');
|
|
163
110
|
assert.equal(issue42.status, 'In Progress');
|
|
164
111
|
assert.equal(issue42.priority, 'High');
|
|
165
|
-
assert.equal(issue42.storyPoints, 3);
|
|
166
|
-
assert.equal(issue42.sprint, 'Sprint 5');
|
|
167
112
|
assert.equal(issue42.assignee, 'Alice');
|
|
168
|
-
assert.equal(issue42.url,
|
|
113
|
+
assert.equal(issue42.url, 'https://fake-jira.atlassian.net/browse/PROJ-42');
|
|
169
114
|
assert.equal(issue42.projectKey, 'PROJ');
|
|
115
|
+
assert.equal(issue42.updatedAt, '');
|
|
116
|
+
assert.equal(issue42.sprint, null);
|
|
117
|
+
assert.equal(issue42.storyPoints, null);
|
|
170
118
|
const issue10 = data.issues.find((i) => i.key === 'PROJ-10');
|
|
171
119
|
assert.ok(issue10, 'Should contain PROJ-10');
|
|
172
120
|
assert.equal(issue10.priority, null);
|
|
173
|
-
assert.equal(issue10.storyPoints, null);
|
|
174
|
-
assert.equal(issue10.sprint, null);
|
|
175
121
|
assert.equal(issue10.assignee, null);
|
|
176
|
-
|
|
177
|
-
assert.equal(
|
|
178
|
-
assert.equal(data.issues[1]?.key, 'PROJ-10');
|
|
122
|
+
assert.equal(issue10.url, 'https://fake-jira.atlassian.net/browse/PROJ-10');
|
|
123
|
+
assert.equal(issue10.projectKey, 'PROJ');
|
|
179
124
|
});
|
|
180
|
-
test('GET /issues
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
125
|
+
test('GET /issues returns acli_not_in_path when exec throws ENOENT', async () => {
|
|
126
|
+
await stopServer();
|
|
127
|
+
const err = new Error('spawn acli ENOENT');
|
|
128
|
+
err.code = 'ENOENT';
|
|
129
|
+
const exec = makeMockExec({ authError: err });
|
|
130
|
+
await startServer(exec);
|
|
131
|
+
const res = await fetch(`${baseUrl}/integration-jira/issues`);
|
|
132
|
+
assert.equal(res.status, 200);
|
|
133
|
+
const data = (await res.json());
|
|
134
|
+
assert.equal(data.error, 'acli_not_in_path');
|
|
135
|
+
assert.equal(data.issues.length, 0);
|
|
136
|
+
});
|
|
137
|
+
test('GET /issues returns acli_not_authenticated when exec stderr contains auth message', async () => {
|
|
138
|
+
await stopServer();
|
|
139
|
+
const err = Object.assign(new Error('acli auth error'), {
|
|
140
|
+
stderr: "not logged in, use 'acli jira auth login' to authenticate",
|
|
141
|
+
});
|
|
142
|
+
const exec = makeMockExec({ authError: err });
|
|
143
|
+
await startServer(exec);
|
|
144
|
+
const res = await fetch(`${baseUrl}/integration-jira/issues`);
|
|
145
|
+
assert.equal(res.status, 200);
|
|
146
|
+
const data = (await res.json());
|
|
147
|
+
assert.equal(data.error, 'acli_not_authenticated');
|
|
148
|
+
assert.equal(data.issues.length, 0);
|
|
149
|
+
});
|
|
150
|
+
test('GET /issues caches results within TTL — exec called only once for two requests', async () => {
|
|
151
|
+
await stopServer();
|
|
152
|
+
let searchCallCount = 0;
|
|
153
|
+
const baseExec = makeMockExec({
|
|
154
|
+
searchItems: [makeAcliWorkItem({ key: 'CACHE-1', summary: 'Cached issue' })],
|
|
155
|
+
});
|
|
156
|
+
const countingExec = async (...args) => {
|
|
157
|
+
const argv = args[1];
|
|
158
|
+
if (argv.includes('workitem') && argv.includes('search')) {
|
|
159
|
+
searchCallCount++;
|
|
191
160
|
}
|
|
192
|
-
return
|
|
161
|
+
return baseExec(...args);
|
|
193
162
|
};
|
|
194
|
-
await
|
|
195
|
-
await startServer();
|
|
163
|
+
await startServer(countingExec);
|
|
196
164
|
// First request — populates cache
|
|
197
|
-
const firstRes = await
|
|
165
|
+
const firstRes = await fetch(`${baseUrl}/integration-jira/issues`);
|
|
198
166
|
const first = (await firstRes.json());
|
|
199
167
|
assert.equal(first.error, undefined, `Unexpected error: ${first.error}`);
|
|
200
168
|
assert.equal(first.issues.length, 1);
|
|
201
|
-
assert.equal(
|
|
202
|
-
// Second request — should be served from cache, no additional
|
|
203
|
-
const secondRes = await
|
|
169
|
+
assert.equal(searchCallCount, 1, 'search should be called once on first request');
|
|
170
|
+
// Second request — should be served from cache, no additional exec call
|
|
171
|
+
const secondRes = await fetch(`${baseUrl}/integration-jira/issues`);
|
|
204
172
|
const second = (await secondRes.json());
|
|
205
173
|
assert.equal(second.error, undefined, `Unexpected error: ${second.error}`);
|
|
206
174
|
assert.equal(second.issues.length, 1);
|
|
207
|
-
assert.equal(
|
|
175
|
+
assert.equal(searchCallCount, 1, 'search should not be called again within TTL (cache hit)');
|
|
208
176
|
});
|
|
209
|
-
test('GET /
|
|
210
|
-
clearEnvVars();
|
|
211
|
-
// fetch should never be called — use a mock that throws to verify early return
|
|
212
|
-
globalThis.fetch = async () => {
|
|
213
|
-
throw new Error('fetch should not be called when not configured');
|
|
214
|
-
};
|
|
177
|
+
test('GET /statuses?projectKey=TEST returns deduplicated statuses', async () => {
|
|
215
178
|
await stopServer();
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
});
|
|
223
|
-
test('GET /statuses?projectKey=TEST returns deduplicated statuses from Jira project API', async () => {
|
|
224
|
-
setEnvVars();
|
|
225
|
-
// Jira returns one entry per issue type; statuses may overlap across types
|
|
226
|
-
const mockProjectStatuses = [
|
|
227
|
-
{
|
|
228
|
-
statuses: [
|
|
229
|
-
{ id: '1', name: 'To Do' },
|
|
230
|
-
{ id: '2', name: 'In Progress' },
|
|
231
|
-
],
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
statuses: [
|
|
235
|
-
{ id: '2', name: 'In Progress' }, // duplicate — should be filtered
|
|
236
|
-
{ id: '3', name: 'Done' },
|
|
237
|
-
],
|
|
238
|
-
},
|
|
179
|
+
// Items with overlapping status IDs — deduplication should collapse to 3 unique statuses
|
|
180
|
+
const searchItems = [
|
|
181
|
+
makeAcliWorkItem({ key: 'TEST-1', statusId: '1', statusName: 'To Do' }),
|
|
182
|
+
makeAcliWorkItem({ key: 'TEST-2', statusId: '2', statusName: 'In Progress' }),
|
|
183
|
+
makeAcliWorkItem({ key: 'TEST-3', statusId: '2', statusName: 'In Progress' }), // duplicate
|
|
184
|
+
makeAcliWorkItem({ key: 'TEST-4', statusId: '3', statusName: 'Done' }),
|
|
239
185
|
];
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return makeJsonResponse(mockProjectStatuses);
|
|
244
|
-
}
|
|
245
|
-
return makeJsonResponse({}, 500);
|
|
246
|
-
};
|
|
247
|
-
await stopServer();
|
|
248
|
-
await startServer();
|
|
249
|
-
const res = await realFetch(`${baseUrl}/integration-jira/statuses?projectKey=TEST`);
|
|
186
|
+
const exec = makeMockExec({ searchItems });
|
|
187
|
+
await startServer(exec);
|
|
188
|
+
const res = await fetch(`${baseUrl}/integration-jira/statuses?projectKey=TEST`);
|
|
250
189
|
assert.equal(res.status, 200);
|
|
251
190
|
const data = (await res.json());
|
|
252
191
|
assert.equal(data.error, undefined, `Unexpected error: ${data.error}`);
|
|
@@ -256,47 +195,27 @@ test('GET /statuses?projectKey=TEST returns deduplicated statuses from Jira proj
|
|
|
256
195
|
const names = data.statuses.map((s) => s.name);
|
|
257
196
|
assert.deepEqual(names, ['To Do', 'In Progress', 'Done']);
|
|
258
197
|
});
|
|
259
|
-
test('GET /issues returns jira_auth_failed on 401 from Jira API', async () => {
|
|
260
|
-
setEnvVars();
|
|
261
|
-
globalThis.fetch = async (input) => {
|
|
262
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
263
|
-
if (url.includes('/rest/api/3/search')) {
|
|
264
|
-
return makeJsonResponse({ message: 'Unauthorized' }, 401);
|
|
265
|
-
}
|
|
266
|
-
return makeJsonResponse({}, 500);
|
|
267
|
-
};
|
|
268
|
-
await stopServer();
|
|
269
|
-
await startServer();
|
|
270
|
-
const res = await realFetch(`${baseUrl}/integration-jira/issues`);
|
|
271
|
-
assert.equal(res.status, 200);
|
|
272
|
-
const data = (await res.json());
|
|
273
|
-
assert.equal(data.error, 'jira_auth_failed');
|
|
274
|
-
assert.equal(data.issues.length, 0);
|
|
275
|
-
});
|
|
276
|
-
test('GET /statuses returns jira_auth_failed on 401 from Jira project API', async () => {
|
|
277
|
-
setEnvVars();
|
|
278
|
-
globalThis.fetch = async (input) => {
|
|
279
|
-
const url = typeof input === 'string' ? input : input.toString();
|
|
280
|
-
if (url.includes('/rest/api/3/project')) {
|
|
281
|
-
return makeJsonResponse({ message: 'Unauthorized' }, 401);
|
|
282
|
-
}
|
|
283
|
-
return makeJsonResponse({}, 500);
|
|
284
|
-
};
|
|
285
|
-
await stopServer();
|
|
286
|
-
await startServer();
|
|
287
|
-
const res = await realFetch(`${baseUrl}/integration-jira/statuses?projectKey=TEST`);
|
|
288
|
-
assert.equal(res.status, 200);
|
|
289
|
-
const data = (await res.json());
|
|
290
|
-
assert.equal(data.error, 'jira_auth_failed');
|
|
291
|
-
assert.equal(data.statuses.length, 0);
|
|
292
|
-
});
|
|
293
198
|
test('GET /statuses returns 400 when projectKey query param is missing', async () => {
|
|
294
|
-
setEnvVars();
|
|
295
|
-
globalThis.fetch = realFetch;
|
|
296
199
|
await stopServer();
|
|
297
|
-
|
|
298
|
-
|
|
200
|
+
const exec = makeMockExec({ searchItems: [] });
|
|
201
|
+
await startServer(exec);
|
|
202
|
+
const res = await fetch(`${baseUrl}/integration-jira/statuses`);
|
|
299
203
|
assert.equal(res.status, 400);
|
|
300
204
|
const data = (await res.json());
|
|
301
205
|
assert.equal(data.error, 'missing_project_key');
|
|
302
206
|
});
|
|
207
|
+
test('GET /statuses returns 400 for invalid projectKey (lowercase or special chars)', async () => {
|
|
208
|
+
await stopServer();
|
|
209
|
+
const exec = makeMockExec({ searchItems: [] });
|
|
210
|
+
await startServer(exec);
|
|
211
|
+
// Lowercase key
|
|
212
|
+
const resLower = await fetch(`${baseUrl}/integration-jira/statuses?projectKey=test`);
|
|
213
|
+
assert.equal(resLower.status, 400);
|
|
214
|
+
const dataLower = (await resLower.json());
|
|
215
|
+
assert.equal(dataLower.error, 'invalid_project_key');
|
|
216
|
+
// Key with special characters
|
|
217
|
+
const resSpecial = await fetch(`${baseUrl}/integration-jira/statuses?projectKey=TEST%20PROJ`);
|
|
218
|
+
assert.equal(resSpecial.status, 400);
|
|
219
|
+
const dataSpecial = (await resSpecial.json());
|
|
220
|
+
assert.equal(dataSpecial.error, 'invalid_project_key');
|
|
221
|
+
});
|