gogcli-mcp 2.30.0 → 4.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +15345 -18417
- package/dist/lib.js +13720 -9481
- package/manifest.json +2 -2
- package/mint.yaml +37 -33
- package/package.json +5 -5
- package/server.json +2 -2
- package/src/attachments.ts +28 -34
- package/src/blob-upload.ts +165 -134
- package/src/blob-urls.ts +3 -5
- package/src/bootstrap-auth.ts +97 -0
- package/src/gmail-dispatch-guard.ts +106 -0
- package/src/gmail-results.ts +1 -1
- package/src/index.ts +3 -4
- package/src/lib.ts +24 -9
- package/src/pagination.ts +1 -1
- package/src/runner.ts +23 -175
- package/src/tools/api.ts +7 -7
- package/src/tools/appscript.ts +16 -16
- package/src/tools/auth.ts +16 -16
- package/src/tools/calendar.ts +13 -13
- package/src/tools/chat.ts +25 -25
- package/src/tools/classroom.ts +49 -49
- package/src/tools/contacts.ts +9 -9
- package/src/tools/docs.ts +13 -13
- package/src/tools/drive.ts +23 -25
- package/src/tools/gmail.ts +145 -32
- package/src/tools/sheets.ts +15 -15
- package/src/tools/slides.ts +13 -13
- package/src/tools/tasks.ts +13 -13
- package/src/tools/utils.ts +14 -61
- package/tests/attachments.test.ts +11 -14
- package/tests/blob-upload.test.ts +235 -160
- package/tests/bootstrap-auth.test.ts +245 -0
- package/tests/gmail-dispatch-guard.test.ts +132 -0
- package/tests/runner-file-args.test.ts +1 -13
- package/tests/runner.test.ts +8 -95
- package/tests/sdk-single-copy.test.ts +11 -37
- package/tests/tools/appscript.test.ts +1 -1
- package/tests/tools/auth-401-shapes.test.ts +2 -3
- package/tests/tools/auth.test.ts +5 -4
- package/tests/tools/chat.test.ts +1 -1
- package/tests/tools/drive.test.ts +11 -3
- package/tests/tools/gmail.test.ts +244 -14
- package/tests/tools/sheets.test.ts +1 -1
- package/tests/tools/utils.test.ts +1 -50
- package/tests/zod-single-copy.test.ts +8 -16
- package/tsconfig.json +1 -4
- package/vitest.config.ts +2 -14
- package/src/auth-log.ts +0 -205
- package/src/connector-auth.ts +0 -303
- package/src/connector-runtime.ts +0 -887
- package/src/google-probe.ts +0 -113
- package/src/google-token.ts +0 -391
- package/src/remote-runner.ts +0 -77
- package/src/worker.ts +0 -129
- package/tests/auth-log.test.ts +0 -530
- package/tests/connector-auth.test.ts +0 -559
- package/tests/connector-runtime.test.ts +0 -1644
- package/tests/google-probe.test.ts +0 -116
- package/tests/google-token.test.ts +0 -425
- package/tests/remote-runner.test.ts +0 -202
- package/tests/worker.test.ts +0 -167
|
@@ -1,204 +1,279 @@
|
|
|
1
|
-
import { describe, it, expect,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
3
|
+
import type { AddressInfo } from 'node:net';
|
|
4
|
+
import { mkdtemp, mkdir, writeFile, symlink, truncate, rm } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
uploadToBlobStore,
|
|
9
|
+
ATTACHMENT_DOWNLOAD_ROOT,
|
|
10
|
+
BLOB_UPLOAD_TIMEOUT_MS,
|
|
11
|
+
MAX_BLOB_UPLOAD_BYTES,
|
|
12
|
+
} from '../src/blob-upload.js';
|
|
3
13
|
|
|
4
|
-
// A minted PUT URL, with the two shapes the scrubber has to recognise: the
|
|
5
|
-
// whole string, and the bare signature it carries.
|
|
6
14
|
const SIG = 'AbCdEf-_1234';
|
|
7
|
-
const
|
|
15
|
+
const CONTENT = Buffer.from('%PDF-1.7 twelve bytes and a few more');
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
interface Received {
|
|
18
|
+
method?: string;
|
|
19
|
+
headers: IncomingMessage['headers'];
|
|
20
|
+
body: Buffer;
|
|
21
|
+
}
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
let server: Server;
|
|
24
|
+
let base: string;
|
|
25
|
+
let received: Received[];
|
|
26
|
+
let handler: (req: IncomingMessage, res: ServerResponse, body: Buffer) => void;
|
|
27
|
+
let root: string;
|
|
28
|
+
let outside: string;
|
|
12
29
|
|
|
13
|
-
function
|
|
14
|
-
return
|
|
15
|
-
ok,
|
|
16
|
-
status,
|
|
17
|
-
text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
|
|
18
|
-
}));
|
|
30
|
+
function signedUrl(path = '/b/reg_1/gmail/m1/a1/Guest%20Copy.pdf'): string {
|
|
31
|
+
return `${base}${path}?exp=1757000000000&sig=${SIG}`;
|
|
19
32
|
}
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
received = [];
|
|
36
|
+
handler = (_req, res) => { res.writeHead(200); res.end(); };
|
|
37
|
+
server = createServer((req, res) => {
|
|
38
|
+
const chunks: Buffer[] = [];
|
|
39
|
+
req.on('data', (c: Buffer) => chunks.push(c));
|
|
40
|
+
req.on('end', () => {
|
|
41
|
+
const body = Buffer.concat(chunks);
|
|
42
|
+
received.push({ method: req.method, headers: req.headers, body });
|
|
43
|
+
handler(req, res, body);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
47
|
+
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
48
|
+
|
|
49
|
+
const scratch = await mkdtemp(join(tmpdir(), 'blob-upload-'));
|
|
50
|
+
root = join(scratch, 'gog-attachments');
|
|
51
|
+
outside = join(scratch, 'outside');
|
|
52
|
+
await mkdir(join(root, 'm1', 'a1'), { recursive: true });
|
|
53
|
+
await mkdir(outside);
|
|
54
|
+
await writeFile(join(root, 'm1', 'a1', 'Guest_Copy.pdf'), CONTENT);
|
|
55
|
+
await writeFile(join(outside, 'refresh-token'), 'secret');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
server.closeAllConnections();
|
|
60
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
61
|
+
await rm(join(root, '..'), { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const file = () => join(root, 'm1', 'a1', 'Guest_Copy.pdf');
|
|
65
|
+
const request = (overrides: Partial<{ path: string; url: string; contentType: string }> = {}) => ({
|
|
66
|
+
path: file(),
|
|
67
|
+
url: signedUrl(),
|
|
68
|
+
contentType: 'application/pdf',
|
|
69
|
+
...overrides,
|
|
24
70
|
});
|
|
71
|
+
const failure = (promise: Promise<unknown>) => promise.then(
|
|
72
|
+
() => { throw new Error('expected the upload to fail'); },
|
|
73
|
+
(err: Error) => err,
|
|
74
|
+
);
|
|
25
75
|
|
|
26
76
|
describe('uploadToBlobStore', () => {
|
|
27
|
-
it('
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
expect(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(
|
|
39
|
-
|
|
40
|
-
'Content-Type': 'application/json',
|
|
41
|
-
});
|
|
42
|
-
expect(JSON.parse(init.body as string)).toEqual({
|
|
43
|
-
path: REQUEST.path,
|
|
44
|
-
url: PUT_URL,
|
|
45
|
-
contentType: 'application/pdf',
|
|
46
|
-
});
|
|
47
|
-
// A deadline of our own: the runner's 120 s is an INACTIVITY timer and
|
|
48
|
-
// cannot bound a transfer that keeps dribbling, so nothing else protects
|
|
49
|
-
// the MCP caller from a request that never ends.
|
|
50
|
-
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
77
|
+
it('PUTs the file to the signed URL and reports the size streamed and the status', async () => {
|
|
78
|
+
handler = (_req, res) => { res.writeHead(201); res.end(); };
|
|
79
|
+
|
|
80
|
+
const outcome = await uploadToBlobStore(request(), { root });
|
|
81
|
+
|
|
82
|
+
expect(outcome).toEqual({ bytes: CONTENT.length, status: 201 });
|
|
83
|
+
expect(received).toHaveLength(1);
|
|
84
|
+
expect(received[0].method).toBe('PUT');
|
|
85
|
+
expect(received[0].body.equals(CONTENT)).toBe(true);
|
|
86
|
+
// Content-Length from the file's own size — a chunked PUT is a 411 at the
|
|
87
|
+
// gateway, so this must never be left to the transport.
|
|
88
|
+
expect(received[0].headers['content-length']).toBe(String(CONTENT.length));
|
|
89
|
+
expect(received[0].headers['transfer-encoding']).toBeUndefined();
|
|
51
90
|
});
|
|
52
91
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
// The PUT signature commits to the content type, so any normalisation —
|
|
93
|
+
// re-casing, a charset appended, a default substituted — is a refusal that
|
|
94
|
+
// reads like a missing object.
|
|
95
|
+
it('sends the content type byte for byte as given', async () => {
|
|
96
|
+
await uploadToBlobStore(request({ contentType: 'Application/PDF; Name="x"' }), { root });
|
|
56
97
|
|
|
57
|
-
|
|
58
|
-
.rejects.toThrow(/GOG_RUNNER_URL.*GOG_RUNNER_KEY/s);
|
|
59
|
-
await expect(uploadToBlobStore(REQUEST, { env: { GOG_RUNNER_KEY: ENV.GOG_RUNNER_KEY } }))
|
|
60
|
-
.rejects.toThrow(/GOG_RUNNER_URL.*GOG_RUNNER_KEY/s);
|
|
61
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
98
|
+
expect(received[0].headers['content-type']).toBe('Application/PDF; Name="x"');
|
|
62
99
|
});
|
|
63
100
|
|
|
64
|
-
it('
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
status: 403,
|
|
68
|
-
retryable: false,
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
await expect(uploadToBlobStore(REQUEST, { env: ENV }))
|
|
72
|
-
.rejects.toThrow(/the blob store refused the upload with 403: forbidden/);
|
|
101
|
+
it('accepts a path relative to the root', async () => {
|
|
102
|
+
await expect(uploadToBlobStore(request({ path: 'm1/a1/Guest_Copy.pdf' }), { root }))
|
|
103
|
+
.resolves.toEqual({ bytes: CONTENT.length, status: 200 });
|
|
73
104
|
});
|
|
74
105
|
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
describe('confinement to the download root', () => {
|
|
107
|
+
it('refuses a path that walks out of the root, without dialling', async () => {
|
|
108
|
+
const err = await failure(uploadToBlobStore(
|
|
109
|
+
request({ path: join(root, '..', 'outside', 'refresh-token') }), { root },
|
|
110
|
+
));
|
|
111
|
+
|
|
112
|
+
expect(err.message).toMatch(/must be inside/);
|
|
113
|
+
expect(received).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('refuses a sibling that merely shares the root as a prefix', async () => {
|
|
117
|
+
await mkdir(`${root}-evil`);
|
|
118
|
+
await writeFile(`${root}-evil/x`, 'nope');
|
|
119
|
+
|
|
120
|
+
const err = await failure(uploadToBlobStore(request({ path: `${root}-evil/x` }), { root }));
|
|
121
|
+
|
|
122
|
+
expect(err.message).toMatch(/must be inside/);
|
|
123
|
+
expect(received).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('refuses a symlink inside the root that points outside it', async () => {
|
|
127
|
+
await symlink(join(outside, 'refresh-token'), join(root, 'm1', 'escape'));
|
|
128
|
+
|
|
129
|
+
const err = await failure(uploadToBlobStore(request({ path: join(root, 'm1', 'escape') }), { root }));
|
|
130
|
+
|
|
131
|
+
expect(err.message).toMatch(/must be inside/);
|
|
132
|
+
expect(received).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('refuses a directory', async () => {
|
|
136
|
+
const err = await failure(uploadToBlobStore(request({ path: join(root, 'm1') }), { root }));
|
|
137
|
+
|
|
138
|
+
expect(err.message).toMatch(/regular file/);
|
|
139
|
+
expect(received).toHaveLength(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('says the file is missing, rather than failing somewhere later', async () => {
|
|
143
|
+
const err = await failure(uploadToBlobStore(request({ path: join(root, 'm1', 'gone.pdf') }), { root }));
|
|
144
|
+
|
|
145
|
+
expect(err.message).toMatch(/could not be found/);
|
|
146
|
+
expect(received).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('confines to the real attachment root when no root is given', async () => {
|
|
150
|
+
expect(ATTACHMENT_DOWNLOAD_ROOT).toBe('/tmp/gog-attachments');
|
|
77
151
|
|
|
78
|
-
|
|
152
|
+
const err = await failure(uploadToBlobStore(request({ path: '/etc/passwd' })));
|
|
153
|
+
|
|
154
|
+
expect(err.message).toContain('must be inside /tmp/gog-attachments');
|
|
155
|
+
expect(received).toHaveLength(0);
|
|
156
|
+
});
|
|
79
157
|
});
|
|
80
158
|
|
|
81
|
-
|
|
82
|
-
|
|
159
|
+
// mcp-host's blob store answers 413 past 100 MiB, but only after the whole
|
|
160
|
+
// transfer has run — and a caller reads a late size error as a signing one.
|
|
161
|
+
it('refuses a file over the blob store\'s ceiling before dialling', async () => {
|
|
162
|
+
expect(MAX_BLOB_UPLOAD_BYTES).toBe(100 * 1024 * 1024);
|
|
163
|
+
const big = join(root, 'm1', 'a1', 'huge.bin');
|
|
164
|
+
await writeFile(big, '');
|
|
165
|
+
await truncate(big, MAX_BLOB_UPLOAD_BYTES + 1); // sparse: no 100 MiB write
|
|
83
166
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
167
|
+
const err = await failure(uploadToBlobStore(request({ path: big }), { root }));
|
|
168
|
+
|
|
169
|
+
expect(err.message).toContain(`${MAX_BLOB_UPLOAD_BYTES + 1} bytes`);
|
|
170
|
+
expect(err.message).toContain(String(MAX_BLOB_UPLOAD_BYTES));
|
|
171
|
+
expect(received).toHaveLength(0);
|
|
87
172
|
});
|
|
88
173
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const err = await uploadToBlobStore(REQUEST, { env: ENV }).catch((e: Error) => e);
|
|
97
|
-
expect(err).toBeInstanceOf(Error);
|
|
98
|
-
expect((err as Error).message).not.toContain(PUT_URL);
|
|
99
|
-
expect((err as Error).message).not.toContain(SIG);
|
|
100
|
-
expect((err as Error).message).toContain('<signed url>');
|
|
174
|
+
it('refuses a URL that is not absolute http(s), without dialling', async () => {
|
|
175
|
+
for (const url of ['not a url at all', 'ftp://blob.example/x', '']) {
|
|
176
|
+
const err = await failure(uploadToBlobStore(request({ url }), { root }));
|
|
177
|
+
expect(err.message).toMatch(/absolute http\(s\) URL/);
|
|
178
|
+
}
|
|
179
|
+
expect(received).toHaveLength(0);
|
|
101
180
|
});
|
|
102
181
|
|
|
103
|
-
it('
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
182
|
+
it('throws with the blob store\'s status and a bounded quote of its answer', async () => {
|
|
183
|
+
handler = (_req, res) => {
|
|
184
|
+
res.writeHead(403, { 'content-type': 'application/json' });
|
|
185
|
+
res.end(JSON.stringify({ error: 'signature does not verify' }));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const err = await failure(uploadToBlobStore(request(), { root }));
|
|
107
189
|
|
|
108
|
-
|
|
109
|
-
expect(
|
|
110
|
-
expect((err as Error).message).not.toContain(SIG);
|
|
111
|
-
expect((err as Error).message).toContain('ECONNREFUSED');
|
|
190
|
+
expect(err.message).toMatch(/403/);
|
|
191
|
+
expect(err.message).toContain('signature does not verify');
|
|
112
192
|
});
|
|
113
193
|
|
|
114
|
-
it('
|
|
115
|
-
|
|
194
|
+
it('quotes no more than a snippet of a long error body', async () => {
|
|
195
|
+
handler = (_req, res) => {
|
|
196
|
+
res.writeHead(500);
|
|
197
|
+
res.write('x'.repeat(400));
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
res.write('y'.repeat(400));
|
|
200
|
+
setTimeout(() => res.end('z'.repeat(400)), 20);
|
|
201
|
+
}, 20);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const err = await failure(uploadToBlobStore(request(), { root }));
|
|
205
|
+
|
|
206
|
+
expect(err.message).toMatch(/500/);
|
|
207
|
+
expect(err.message).not.toContain('z');
|
|
208
|
+
expect(err.message.length).toBeLessThan(1000);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('gives up at its total deadline rather than hanging the MCP request', async () => {
|
|
212
|
+
handler = () => { /* never answers */ };
|
|
213
|
+
|
|
214
|
+
const err = await failure(uploadToBlobStore(request(), { root, timeoutMs: 50 }));
|
|
116
215
|
|
|
117
|
-
|
|
216
|
+
expect(err.message).toMatch(/did not finish within 50ms/);
|
|
118
217
|
});
|
|
119
218
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// module's contract is about every one of them: nothing it throws carries the
|
|
123
|
-
// URL. undici's own body errors ('terminated', 'Premature close') happen to
|
|
124
|
-
// name nothing, so this is the guarantee being a property of the function
|
|
125
|
-
// rather than a fact about today's runtime.
|
|
126
|
-
it('scrubs a body that fails to read, as it scrubs a failed dial', async () => {
|
|
127
|
-
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
128
|
-
ok: true,
|
|
129
|
-
status: 200,
|
|
130
|
-
text: async () => { throw new Error(`terminated while reading ${PUT_URL}`); },
|
|
131
|
-
})));
|
|
132
|
-
|
|
133
|
-
const err = await uploadToBlobStore(REQUEST, { env: ENV }).catch((e: Error) => e);
|
|
134
|
-
expect(err).toBeInstanceOf(Error);
|
|
135
|
-
expect((err as Error).message).not.toContain(PUT_URL);
|
|
136
|
-
expect((err as Error).message).not.toContain(SIG);
|
|
137
|
-
expect((err as Error).message).toContain('terminated');
|
|
219
|
+
it('keeps a deadline of its own by default', () => {
|
|
220
|
+
expect(BLOB_UPLOAD_TIMEOUT_MS).toBeGreaterThan(30_000);
|
|
138
221
|
});
|
|
139
222
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})));
|
|
223
|
+
// A signed URL is a credential with up to 24 h of anybody-who-holds-it access
|
|
224
|
+
// to the object, and everything thrown here is expected to be logged.
|
|
225
|
+
describe('never lets the signed URL or its bare signature into a message', () => {
|
|
226
|
+
it('scrubs the blob store quoting the request back', async () => {
|
|
227
|
+
handler = (req, res) => { res.writeHead(403); res.end(`refused PUT ${base}${req.url} (sig ${SIG})`); };
|
|
146
228
|
|
|
147
|
-
|
|
148
|
-
});
|
|
229
|
+
const err = await failure(uploadToBlobStore(request(), { root }));
|
|
149
230
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const odd = 'not a url at all';
|
|
155
|
-
vi.stubGlobal('fetch', answers(422, { error: `refused: ${odd}`, status: 400 }));
|
|
231
|
+
expect(err.message).not.toContain(signedUrl());
|
|
232
|
+
expect(err.message).not.toContain(SIG);
|
|
233
|
+
expect(err.message).toContain('<signed url>');
|
|
234
|
+
});
|
|
156
235
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
236
|
+
it('scrubs a transport failure', async () => {
|
|
237
|
+
server.closeAllConnections();
|
|
238
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
239
|
+
server = createServer();
|
|
240
|
+
server.listen(0);
|
|
161
241
|
|
|
162
|
-
|
|
163
|
-
// replacement between every one of them, so an empty needle does not redact a
|
|
164
|
-
// message, it destroys it. Today's one call site always mints a URL first —
|
|
165
|
-
// this is a property of the function, which is why it is asserted on it.
|
|
166
|
-
it('an empty URL leaves the message intact rather than shredding it', async () => {
|
|
167
|
-
vi.stubGlobal('fetch', answers(422, { error: 'the blob store refused the upload', status: 403 }));
|
|
242
|
+
const err = await failure(uploadToBlobStore(request(), { root }));
|
|
168
243
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
});
|
|
244
|
+
expect(err.message).toMatch(/ECONNREFUSED/);
|
|
245
|
+
expect(err.message).not.toContain(SIG);
|
|
246
|
+
});
|
|
173
247
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
248
|
+
it('scrubs a response severed mid-body', async () => {
|
|
249
|
+
handler = (_req, res) => {
|
|
250
|
+
res.writeHead(403, { 'content-length': '1000' });
|
|
251
|
+
res.write('partial', () => res.socket?.destroy());
|
|
252
|
+
};
|
|
179
253
|
|
|
180
|
-
|
|
181
|
-
expect((fetchMock.mock.calls[0] as [string, RequestInit])[0]).toBe('https://runner.example/upload');
|
|
182
|
-
});
|
|
254
|
+
const err = await failure(uploadToBlobStore(request(), { root }));
|
|
183
255
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
256
|
+
expect(err.message).toMatch(/upload to the blob store did not complete/);
|
|
257
|
+
expect(err.message).not.toContain(SIG);
|
|
258
|
+
});
|
|
187
259
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
260
|
+
it('scrubs a TLS failure on an https URL', async () => {
|
|
261
|
+
const url = signedUrl().replace('http:', 'https:');
|
|
262
|
+
|
|
263
|
+
const err = await failure(uploadToBlobStore(request({ url }), { root }));
|
|
264
|
+
|
|
265
|
+
expect(err.message).toMatch(/upload to the blob store did not complete/);
|
|
266
|
+
expect(err.message).not.toContain(url);
|
|
267
|
+
expect(err.message).not.toContain(SIG);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// `split('')` would cut the message into characters and interleave the
|
|
271
|
+
// replacement between every one of them.
|
|
272
|
+
it('leaves a message intact when the URL is empty', async () => {
|
|
273
|
+
const err = await failure(uploadToBlobStore(request({ url: '' }), { root }));
|
|
274
|
+
|
|
275
|
+
expect(err.message).not.toContain('<signed url>');
|
|
276
|
+
expect(err.message).toMatch(/absolute http\(s\) URL/);
|
|
277
|
+
});
|
|
203
278
|
});
|
|
204
279
|
});
|