@positronic/cloudflare 0.0.2 → 0.0.4
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/src/api.js +1270 -0
- package/dist/src/brain-runner-do.js +654 -0
- package/dist/src/dev-server.js +1357 -0
- package/{src/index.ts → dist/src/index.js} +1 -6
- package/dist/src/manifest.js +278 -0
- package/dist/src/monitor-do.js +408 -0
- package/{src/node-index.ts → dist/src/node-index.js} +3 -7
- package/dist/src/r2-loader.js +207 -0
- package/dist/src/schedule-do.js +705 -0
- package/dist/src/sqlite-adapter.js +69 -0
- package/dist/types/api.d.ts +21 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/brain-runner-do.d.ts +25 -0
- package/dist/types/brain-runner-do.d.ts.map +1 -0
- package/dist/types/dev-server.d.ts +45 -0
- package/dist/types/dev-server.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/manifest.d.ts +11 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/monitor-do.d.ts +16 -0
- package/dist/types/monitor-do.d.ts.map +1 -0
- package/dist/types/node-index.d.ts +10 -0
- package/dist/types/node-index.d.ts.map +1 -0
- package/dist/types/r2-loader.d.ts +10 -0
- package/dist/types/r2-loader.d.ts.map +1 -0
- package/dist/types/schedule-do.d.ts +47 -0
- package/dist/types/schedule-do.d.ts.map +1 -0
- package/dist/types/sqlite-adapter.d.ts +10 -0
- package/dist/types/sqlite-adapter.d.ts.map +1 -0
- package/package.json +8 -4
- package/src/api.ts +0 -579
- package/src/brain-runner-do.ts +0 -309
- package/src/dev-server.ts +0 -776
- package/src/manifest.ts +0 -69
- package/src/monitor-do.ts +0 -268
- package/src/r2-loader.ts +0 -27
- package/src/schedule-do.ts +0 -377
- package/src/sqlite-adapter.ts +0 -50
- package/test-project/package-lock.json +0 -3010
- package/test-project/package.json +0 -21
- package/test-project/src/index.ts +0 -70
- package/test-project/src/runner.ts +0 -24
- package/test-project/tests/api.test.ts +0 -1005
- package/test-project/tests/r2loader.test.ts +0 -73
- package/test-project/tests/resources-api.test.ts +0 -671
- package/test-project/tests/spec.test.ts +0 -135
- package/test-project/tests/tsconfig.json +0 -7
- package/test-project/tsconfig.json +0 -20
- package/test-project/vitest.config.ts +0 -12
- package/test-project/wrangler.jsonc +0 -53
- package/tsconfig.json +0 -11
|
@@ -1,671 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
env,
|
|
3
|
-
createExecutionContext,
|
|
4
|
-
waitOnExecutionContext,
|
|
5
|
-
} from 'cloudflare:test';
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
8
|
-
import worker from '../src/index';
|
|
9
|
-
|
|
10
|
-
interface TestEnv {
|
|
11
|
-
RESOURCES_BUCKET: R2Bucket;
|
|
12
|
-
NODE_ENV?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('Resources API Tests', () => {
|
|
16
|
-
const testEnv = env as TestEnv;
|
|
17
|
-
|
|
18
|
-
beforeAll(async () => {
|
|
19
|
-
// Clean up any existing test resources
|
|
20
|
-
try {
|
|
21
|
-
const listed = await testEnv.RESOURCES_BUCKET.list();
|
|
22
|
-
for (const obj of listed.objects) {
|
|
23
|
-
await testEnv.RESOURCES_BUCKET.delete(obj.key);
|
|
24
|
-
}
|
|
25
|
-
} catch (e) {
|
|
26
|
-
// Ignore errors if bucket is empty
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
afterAll(async () => {
|
|
31
|
-
// Clean up after all tests
|
|
32
|
-
try {
|
|
33
|
-
const listed = await testEnv.RESOURCES_BUCKET.list();
|
|
34
|
-
for (const obj of listed.objects) {
|
|
35
|
-
await testEnv.RESOURCES_BUCKET.delete(obj.key);
|
|
36
|
-
}
|
|
37
|
-
} catch (e) {
|
|
38
|
-
// Ignore errors
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('POST /resources with path only', async () => {
|
|
43
|
-
const formData = new FormData();
|
|
44
|
-
formData.append(
|
|
45
|
-
'file',
|
|
46
|
-
new Blob(['Hello from test file'], { type: 'text/plain' }),
|
|
47
|
-
'test.txt'
|
|
48
|
-
);
|
|
49
|
-
formData.append('type', 'text');
|
|
50
|
-
formData.append('path', 'resources/test-files/test.txt');
|
|
51
|
-
formData.append('local', 'false');
|
|
52
|
-
|
|
53
|
-
const request = new Request('http://example.com/resources', {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
body: formData,
|
|
56
|
-
});
|
|
57
|
-
const context = createExecutionContext();
|
|
58
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
59
|
-
await waitOnExecutionContext(context);
|
|
60
|
-
|
|
61
|
-
expect(response.status).toBe(201);
|
|
62
|
-
const responseBody = await response.json<{
|
|
63
|
-
type: string;
|
|
64
|
-
path: string;
|
|
65
|
-
key: string;
|
|
66
|
-
size: number;
|
|
67
|
-
lastModified: string;
|
|
68
|
-
local: boolean;
|
|
69
|
-
}>();
|
|
70
|
-
expect(responseBody).toEqual({
|
|
71
|
-
type: 'text',
|
|
72
|
-
path: 'resources/test-files/test.txt',
|
|
73
|
-
key: 'resources/test-files/test.txt',
|
|
74
|
-
size: expect.any(Number),
|
|
75
|
-
lastModified: expect.any(String),
|
|
76
|
-
local: false,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Verify the resource was actually stored in R2
|
|
80
|
-
const storedObject = await testEnv.RESOURCES_BUCKET.get(
|
|
81
|
-
'resources/test-files/test.txt'
|
|
82
|
-
);
|
|
83
|
-
expect(storedObject).not.toBeNull();
|
|
84
|
-
const storedText = await storedObject!.text();
|
|
85
|
-
expect(storedText).toBe('Hello from test file');
|
|
86
|
-
expect(storedObject!.customMetadata).toEqual({
|
|
87
|
-
type: 'text',
|
|
88
|
-
path: 'resources/test-files/test.txt',
|
|
89
|
-
local: 'false',
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('POST /resources with key only (no path)', async () => {
|
|
94
|
-
const formData = new FormData();
|
|
95
|
-
const videoContent = new Uint8Array([
|
|
96
|
-
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70,
|
|
97
|
-
]); // Mock video header
|
|
98
|
-
formData.append(
|
|
99
|
-
'file',
|
|
100
|
-
new Blob([videoContent], { type: 'video/mp4' }),
|
|
101
|
-
'video.mp4'
|
|
102
|
-
);
|
|
103
|
-
formData.append('type', 'binary');
|
|
104
|
-
formData.append('key', 'videos/large-video.mp4');
|
|
105
|
-
formData.append('local', 'false');
|
|
106
|
-
|
|
107
|
-
const request = new Request('http://example.com/resources', {
|
|
108
|
-
method: 'POST',
|
|
109
|
-
body: formData,
|
|
110
|
-
});
|
|
111
|
-
const context = createExecutionContext();
|
|
112
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
113
|
-
await waitOnExecutionContext(context);
|
|
114
|
-
|
|
115
|
-
expect(response.status).toBe(201);
|
|
116
|
-
const responseBody = await response.json<{
|
|
117
|
-
type: string;
|
|
118
|
-
path?: string;
|
|
119
|
-
key: string;
|
|
120
|
-
size: number;
|
|
121
|
-
lastModified: string;
|
|
122
|
-
local: boolean;
|
|
123
|
-
}>();
|
|
124
|
-
expect(responseBody).toEqual({
|
|
125
|
-
type: 'binary',
|
|
126
|
-
key: 'videos/large-video.mp4',
|
|
127
|
-
size: expect.any(Number),
|
|
128
|
-
lastModified: expect.any(String),
|
|
129
|
-
local: false,
|
|
130
|
-
});
|
|
131
|
-
// Should not have path since we didn't provide one
|
|
132
|
-
expect(responseBody.path).toBeUndefined();
|
|
133
|
-
|
|
134
|
-
// Verify the resource was stored without path metadata
|
|
135
|
-
const storedObject = await testEnv.RESOURCES_BUCKET.get(
|
|
136
|
-
'videos/large-video.mp4'
|
|
137
|
-
);
|
|
138
|
-
expect(storedObject).not.toBeNull();
|
|
139
|
-
// IMPORTANT: Consume the response body to avoid isolated storage issues
|
|
140
|
-
await storedObject!.arrayBuffer();
|
|
141
|
-
expect(storedObject!.customMetadata).toEqual({
|
|
142
|
-
type: 'binary',
|
|
143
|
-
local: 'false',
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('POST /resources with both key and path (key takes precedence)', async () => {
|
|
148
|
-
const formData = new FormData();
|
|
149
|
-
formData.append(
|
|
150
|
-
'file',
|
|
151
|
-
new Blob(['Image data'], { type: 'image/png' }),
|
|
152
|
-
'logo.png'
|
|
153
|
-
);
|
|
154
|
-
formData.append('type', 'binary');
|
|
155
|
-
formData.append('path', 'resources/images/logo.png');
|
|
156
|
-
formData.append('key', 'assets/branding/logo.png');
|
|
157
|
-
formData.append('local', 'true'); // Testing with local=true
|
|
158
|
-
|
|
159
|
-
const request = new Request('http://example.com/resources', {
|
|
160
|
-
method: 'POST',
|
|
161
|
-
body: formData,
|
|
162
|
-
});
|
|
163
|
-
const context = createExecutionContext();
|
|
164
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
165
|
-
await waitOnExecutionContext(context);
|
|
166
|
-
|
|
167
|
-
expect(response.status).toBe(201);
|
|
168
|
-
const responseBody = await response.json<{
|
|
169
|
-
type: string;
|
|
170
|
-
path: string;
|
|
171
|
-
key: string;
|
|
172
|
-
size: number;
|
|
173
|
-
lastModified: string;
|
|
174
|
-
local: boolean;
|
|
175
|
-
}>();
|
|
176
|
-
expect(responseBody).toEqual({
|
|
177
|
-
type: 'binary',
|
|
178
|
-
path: 'resources/images/logo.png',
|
|
179
|
-
key: 'assets/branding/logo.png',
|
|
180
|
-
size: expect.any(Number),
|
|
181
|
-
lastModified: expect.any(String),
|
|
182
|
-
local: true,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Verify stored at key location, not path location
|
|
186
|
-
const storedAtKey = await testEnv.RESOURCES_BUCKET.get(
|
|
187
|
-
'assets/branding/logo.png'
|
|
188
|
-
);
|
|
189
|
-
expect(storedAtKey).not.toBeNull();
|
|
190
|
-
// IMPORTANT: Consume the response body
|
|
191
|
-
await storedAtKey!.arrayBuffer();
|
|
192
|
-
|
|
193
|
-
const storedAtPath = await testEnv.RESOURCES_BUCKET.get(
|
|
194
|
-
'resources/images/logo.png'
|
|
195
|
-
);
|
|
196
|
-
expect(storedAtPath).toBeNull();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('GET /resources lists all resources correctly', async () => {
|
|
200
|
-
// First create a resource to list
|
|
201
|
-
const formData = new FormData();
|
|
202
|
-
formData.append(
|
|
203
|
-
'file',
|
|
204
|
-
new Blob(['Test content'], { type: 'text/plain' }),
|
|
205
|
-
'test.txt'
|
|
206
|
-
);
|
|
207
|
-
formData.append('type', 'text');
|
|
208
|
-
formData.append('path', 'resources/list-test.txt');
|
|
209
|
-
formData.append('local', 'true');
|
|
210
|
-
|
|
211
|
-
const createRequest = new Request('http://example.com/resources', {
|
|
212
|
-
method: 'POST',
|
|
213
|
-
body: formData,
|
|
214
|
-
});
|
|
215
|
-
const createContext = createExecutionContext();
|
|
216
|
-
await worker.fetch(createRequest, testEnv, createContext);
|
|
217
|
-
await waitOnExecutionContext(createContext);
|
|
218
|
-
|
|
219
|
-
// Now list resources
|
|
220
|
-
const request = new Request('http://example.com/resources');
|
|
221
|
-
const context = createExecutionContext();
|
|
222
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
223
|
-
await waitOnExecutionContext(context);
|
|
224
|
-
|
|
225
|
-
expect(response.status).toBe(200);
|
|
226
|
-
const responseBody = await response.json<{
|
|
227
|
-
resources: Array<{
|
|
228
|
-
type: string;
|
|
229
|
-
path?: string;
|
|
230
|
-
key: string;
|
|
231
|
-
size: number;
|
|
232
|
-
lastModified: string;
|
|
233
|
-
local: boolean;
|
|
234
|
-
}>;
|
|
235
|
-
truncated: boolean;
|
|
236
|
-
count: number;
|
|
237
|
-
}>();
|
|
238
|
-
expect(responseBody.truncated).toBe(false);
|
|
239
|
-
// Should have 1 resource (the one we just created)
|
|
240
|
-
expect(responseBody.count).toBe(1);
|
|
241
|
-
expect(responseBody.resources).toHaveLength(1);
|
|
242
|
-
|
|
243
|
-
// Check that the resource has the correct structure
|
|
244
|
-
const resource = responseBody.resources[0];
|
|
245
|
-
expect(resource).toEqual({
|
|
246
|
-
type: 'text',
|
|
247
|
-
path: 'resources/list-test.txt',
|
|
248
|
-
key: 'resources/list-test.txt',
|
|
249
|
-
size: expect.any(Number),
|
|
250
|
-
lastModified: expect.any(String),
|
|
251
|
-
local: true,
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
describe('Error cases', () => {
|
|
256
|
-
it('POST /resources without file should return 400 error', async () => {
|
|
257
|
-
const formData = new FormData();
|
|
258
|
-
formData.append('type', 'text');
|
|
259
|
-
formData.append('path', 'resources/test.txt');
|
|
260
|
-
|
|
261
|
-
const request = new Request('http://example.com/resources', {
|
|
262
|
-
method: 'POST',
|
|
263
|
-
body: formData,
|
|
264
|
-
});
|
|
265
|
-
const context = createExecutionContext();
|
|
266
|
-
|
|
267
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
268
|
-
expect(response.status).toBe(400);
|
|
269
|
-
|
|
270
|
-
await waitOnExecutionContext(context);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('POST /resources without type should return 400 error', async () => {
|
|
274
|
-
const formData = new FormData();
|
|
275
|
-
formData.append('file', new Blob(['content']), 'test.txt');
|
|
276
|
-
formData.append('path', 'resources/test.txt');
|
|
277
|
-
|
|
278
|
-
const request = new Request('http://example.com/resources', {
|
|
279
|
-
method: 'POST',
|
|
280
|
-
body: formData,
|
|
281
|
-
});
|
|
282
|
-
const context = createExecutionContext();
|
|
283
|
-
|
|
284
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
285
|
-
expect(response.status).toBe(400);
|
|
286
|
-
|
|
287
|
-
await waitOnExecutionContext(context);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('POST /resources with invalid type should return 400 error', async () => {
|
|
291
|
-
const formData = new FormData();
|
|
292
|
-
formData.append('file', new Blob(['content']), 'test.txt');
|
|
293
|
-
formData.append('type', 'invalid');
|
|
294
|
-
formData.append('path', 'resources/test.txt');
|
|
295
|
-
|
|
296
|
-
const request = new Request('http://example.com/resources', {
|
|
297
|
-
method: 'POST',
|
|
298
|
-
body: formData,
|
|
299
|
-
});
|
|
300
|
-
const context = createExecutionContext();
|
|
301
|
-
|
|
302
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
303
|
-
expect(response.status).toBe(400);
|
|
304
|
-
|
|
305
|
-
await waitOnExecutionContext(context);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('POST /resources without key or path should return 400 error', async () => {
|
|
309
|
-
const formData = new FormData();
|
|
310
|
-
formData.append('file', new Blob(['content']), 'test.txt');
|
|
311
|
-
formData.append('type', 'text');
|
|
312
|
-
|
|
313
|
-
const request = new Request('http://example.com/resources', {
|
|
314
|
-
method: 'POST',
|
|
315
|
-
body: formData,
|
|
316
|
-
});
|
|
317
|
-
const context = createExecutionContext();
|
|
318
|
-
|
|
319
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
320
|
-
expect(response.status).toBe(400);
|
|
321
|
-
|
|
322
|
-
await waitOnExecutionContext(context);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('GET /resources with missing type metadata should return 500 error', async () => {
|
|
326
|
-
// Manually create a resource without type metadata
|
|
327
|
-
await testEnv.RESOURCES_BUCKET.put('bad-resource.txt', 'content', {
|
|
328
|
-
customMetadata: {
|
|
329
|
-
path: 'bad-resource.txt',
|
|
330
|
-
// Missing type
|
|
331
|
-
},
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
const request = new Request('http://example.com/resources');
|
|
335
|
-
const context = createExecutionContext();
|
|
336
|
-
|
|
337
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
338
|
-
expect(response.status).toBe(500);
|
|
339
|
-
|
|
340
|
-
await waitOnExecutionContext(context);
|
|
341
|
-
|
|
342
|
-
// Clean up
|
|
343
|
-
await testEnv.RESOURCES_BUCKET.delete('bad-resource.txt');
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe('DELETE /resources/:key', () => {
|
|
348
|
-
it('should delete an existing resource', async () => {
|
|
349
|
-
// First create a resource
|
|
350
|
-
const formData = new FormData();
|
|
351
|
-
formData.append(
|
|
352
|
-
'file',
|
|
353
|
-
new Blob(['Content to delete'], { type: 'text/plain' }),
|
|
354
|
-
'delete-test.txt'
|
|
355
|
-
);
|
|
356
|
-
formData.append('type', 'text');
|
|
357
|
-
formData.append('key', 'resources/delete-test.txt');
|
|
358
|
-
|
|
359
|
-
const createRequest = new Request('http://example.com/resources', {
|
|
360
|
-
method: 'POST',
|
|
361
|
-
body: formData,
|
|
362
|
-
});
|
|
363
|
-
const createContext = createExecutionContext();
|
|
364
|
-
const createResponse = await worker.fetch(
|
|
365
|
-
createRequest,
|
|
366
|
-
testEnv,
|
|
367
|
-
createContext
|
|
368
|
-
);
|
|
369
|
-
await waitOnExecutionContext(createContext);
|
|
370
|
-
expect(createResponse.status).toBe(201);
|
|
371
|
-
|
|
372
|
-
// Now delete it
|
|
373
|
-
const deleteRequest = new Request(
|
|
374
|
-
'http://example.com/resources/' +
|
|
375
|
-
encodeURIComponent('resources/delete-test.txt'),
|
|
376
|
-
{ method: 'DELETE' }
|
|
377
|
-
);
|
|
378
|
-
const deleteContext = createExecutionContext();
|
|
379
|
-
const deleteResponse = await worker.fetch(
|
|
380
|
-
deleteRequest,
|
|
381
|
-
testEnv,
|
|
382
|
-
deleteContext
|
|
383
|
-
);
|
|
384
|
-
await waitOnExecutionContext(deleteContext);
|
|
385
|
-
|
|
386
|
-
expect(deleteResponse.status).toBe(204);
|
|
387
|
-
|
|
388
|
-
// Verify it's deleted
|
|
389
|
-
const deletedObject = await testEnv.RESOURCES_BUCKET.get(
|
|
390
|
-
'resources/delete-test.txt'
|
|
391
|
-
);
|
|
392
|
-
expect(deletedObject).toBeNull();
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('should handle URL encoded keys with slashes', async () => {
|
|
396
|
-
// Create a resource with a path containing subdirectories
|
|
397
|
-
const formData = new FormData();
|
|
398
|
-
formData.append(
|
|
399
|
-
'file',
|
|
400
|
-
new Blob(['Nested content'], { type: 'text/plain' }),
|
|
401
|
-
'nested.txt'
|
|
402
|
-
);
|
|
403
|
-
formData.append('type', 'text');
|
|
404
|
-
formData.append('key', 'resources/subfolder/nested.txt');
|
|
405
|
-
|
|
406
|
-
const createRequest = new Request('http://example.com/resources', {
|
|
407
|
-
method: 'POST',
|
|
408
|
-
body: formData,
|
|
409
|
-
});
|
|
410
|
-
const createContext = createExecutionContext();
|
|
411
|
-
await worker.fetch(createRequest, testEnv, createContext);
|
|
412
|
-
await waitOnExecutionContext(createContext);
|
|
413
|
-
|
|
414
|
-
// Delete with URL encoded key
|
|
415
|
-
const deleteRequest = new Request(
|
|
416
|
-
'http://example.com/resources/' +
|
|
417
|
-
encodeURIComponent('resources/subfolder/nested.txt'),
|
|
418
|
-
{ method: 'DELETE' }
|
|
419
|
-
);
|
|
420
|
-
const deleteContext = createExecutionContext();
|
|
421
|
-
const deleteResponse = await worker.fetch(
|
|
422
|
-
deleteRequest,
|
|
423
|
-
testEnv,
|
|
424
|
-
deleteContext
|
|
425
|
-
);
|
|
426
|
-
await waitOnExecutionContext(deleteContext);
|
|
427
|
-
|
|
428
|
-
expect(deleteResponse.status).toBe(204);
|
|
429
|
-
|
|
430
|
-
// Verify it's deleted
|
|
431
|
-
const deletedObject = await testEnv.RESOURCES_BUCKET.get(
|
|
432
|
-
'resources/subfolder/nested.txt'
|
|
433
|
-
);
|
|
434
|
-
expect(deletedObject).toBeNull();
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it('should return 204 even for non-existent resources (idempotent delete)', async () => {
|
|
438
|
-
const deleteRequest = new Request(
|
|
439
|
-
'http://example.com/resources/' +
|
|
440
|
-
encodeURIComponent('non-existent.txt'),
|
|
441
|
-
{ method: 'DELETE' }
|
|
442
|
-
);
|
|
443
|
-
const deleteContext = createExecutionContext();
|
|
444
|
-
const deleteResponse = await worker.fetch(
|
|
445
|
-
deleteRequest,
|
|
446
|
-
testEnv,
|
|
447
|
-
deleteContext
|
|
448
|
-
);
|
|
449
|
-
await waitOnExecutionContext(deleteContext);
|
|
450
|
-
|
|
451
|
-
// Should return 204 No Content even if resource doesn't exist (idempotent)
|
|
452
|
-
expect(deleteResponse.status).toBe(204);
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
describe('DELETE /resources (bulk delete)', () => {
|
|
457
|
-
beforeEach(async () => {
|
|
458
|
-
// Create some test resources
|
|
459
|
-
const resources = ['file1.txt', 'file2.txt', 'subfolder/file3.txt'];
|
|
460
|
-
|
|
461
|
-
for (const resource of resources) {
|
|
462
|
-
const formData = new FormData();
|
|
463
|
-
formData.append(
|
|
464
|
-
'file',
|
|
465
|
-
new Blob([`Content of ${resource}`], { type: 'text/plain' }),
|
|
466
|
-
resource
|
|
467
|
-
);
|
|
468
|
-
formData.append('type', 'text');
|
|
469
|
-
formData.append('key', `resources/${resource}`);
|
|
470
|
-
|
|
471
|
-
const request = new Request('http://example.com/resources', {
|
|
472
|
-
method: 'POST',
|
|
473
|
-
body: formData,
|
|
474
|
-
});
|
|
475
|
-
const context = createExecutionContext();
|
|
476
|
-
await worker.fetch(request, testEnv, context);
|
|
477
|
-
await waitOnExecutionContext(context);
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('should delete all resources when in development mode', async () => {
|
|
482
|
-
// Explicitly set environment to development mode
|
|
483
|
-
const devEnv = {
|
|
484
|
-
...testEnv,
|
|
485
|
-
NODE_ENV: 'development',
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
const deleteRequest = new Request('http://example.com/resources', {
|
|
489
|
-
method: 'DELETE',
|
|
490
|
-
});
|
|
491
|
-
const deleteContext = createExecutionContext();
|
|
492
|
-
const deleteResponse = await worker.fetch(
|
|
493
|
-
deleteRequest,
|
|
494
|
-
devEnv,
|
|
495
|
-
deleteContext
|
|
496
|
-
);
|
|
497
|
-
await waitOnExecutionContext(deleteContext);
|
|
498
|
-
|
|
499
|
-
expect(deleteResponse.status).toBe(200);
|
|
500
|
-
const responseBody = await deleteResponse.json<{
|
|
501
|
-
deletedCount: number;
|
|
502
|
-
}>();
|
|
503
|
-
expect(responseBody.deletedCount).toBeGreaterThanOrEqual(3);
|
|
504
|
-
|
|
505
|
-
// Verify all resources are deleted
|
|
506
|
-
const listed = await testEnv.RESOURCES_BUCKET.list();
|
|
507
|
-
expect(listed.objects.length).toBe(0);
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it('should return 403 when not in development mode', async () => {
|
|
511
|
-
// Create an environment without NODE_ENV
|
|
512
|
-
const envWithoutNodeEnv = {
|
|
513
|
-
RESOURCES_BUCKET: testEnv.RESOURCES_BUCKET,
|
|
514
|
-
// Explicitly omit NODE_ENV
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const deleteRequest = new Request('http://example.com/resources', {
|
|
518
|
-
method: 'DELETE',
|
|
519
|
-
});
|
|
520
|
-
const deleteContext = createExecutionContext();
|
|
521
|
-
const deleteResponse = await worker.fetch(
|
|
522
|
-
deleteRequest,
|
|
523
|
-
envWithoutNodeEnv,
|
|
524
|
-
deleteContext
|
|
525
|
-
);
|
|
526
|
-
await waitOnExecutionContext(deleteContext);
|
|
527
|
-
|
|
528
|
-
expect(deleteResponse.status).toBe(403);
|
|
529
|
-
const errorBody = await deleteResponse.json<{ error: string }>();
|
|
530
|
-
expect(errorBody.error).toBe(
|
|
531
|
-
'Bulk delete is only available in development mode'
|
|
532
|
-
);
|
|
533
|
-
|
|
534
|
-
// Verify resources are not deleted
|
|
535
|
-
const listed = await testEnv.RESOURCES_BUCKET.list();
|
|
536
|
-
expect(listed.objects.length).toBeGreaterThanOrEqual(3);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it('should return 403 when NODE_ENV is production', async () => {
|
|
540
|
-
const prodEnv = {
|
|
541
|
-
...testEnv,
|
|
542
|
-
NODE_ENV: 'production',
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
const deleteRequest = new Request('http://example.com/resources', {
|
|
546
|
-
method: 'DELETE',
|
|
547
|
-
});
|
|
548
|
-
const deleteContext = createExecutionContext();
|
|
549
|
-
const deleteResponse = await worker.fetch(
|
|
550
|
-
deleteRequest,
|
|
551
|
-
prodEnv,
|
|
552
|
-
deleteContext
|
|
553
|
-
);
|
|
554
|
-
await waitOnExecutionContext(deleteContext);
|
|
555
|
-
|
|
556
|
-
expect(deleteResponse.status).toBe(403);
|
|
557
|
-
const errorBody = await deleteResponse.json<{ error: string }>();
|
|
558
|
-
expect(errorBody.error).toBe(
|
|
559
|
-
'Bulk delete is only available in development mode'
|
|
560
|
-
);
|
|
561
|
-
});
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
describe('POST /resources/presigned-link', () => {
|
|
565
|
-
it('should return 400 when R2 credentials are not configured', async () => {
|
|
566
|
-
// This test verifies behavior when the backend doesn't have credentials configured
|
|
567
|
-
const request = new Request(
|
|
568
|
-
'http://example.com/resources/presigned-link',
|
|
569
|
-
{
|
|
570
|
-
method: 'POST',
|
|
571
|
-
headers: {
|
|
572
|
-
'Content-Type': 'application/json',
|
|
573
|
-
},
|
|
574
|
-
body: JSON.stringify({
|
|
575
|
-
key: 'test-files/large-video.mp4',
|
|
576
|
-
type: 'binary',
|
|
577
|
-
size: 150 * 1024 * 1024, // 150MB
|
|
578
|
-
}),
|
|
579
|
-
}
|
|
580
|
-
);
|
|
581
|
-
const context = createExecutionContext();
|
|
582
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
583
|
-
await waitOnExecutionContext(context);
|
|
584
|
-
|
|
585
|
-
expect(response.status).toBe(400);
|
|
586
|
-
const responseBody = await response.json<{ error: string }>();
|
|
587
|
-
expect(responseBody.error).toBeDefined();
|
|
588
|
-
expect(typeof responseBody.error).toBe('string');
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('should validate required fields in request body', async () => {
|
|
592
|
-
// Missing key
|
|
593
|
-
let request = new Request('http://example.com/resources/presigned-link', {
|
|
594
|
-
method: 'POST',
|
|
595
|
-
headers: {
|
|
596
|
-
'Content-Type': 'application/json',
|
|
597
|
-
},
|
|
598
|
-
body: JSON.stringify({
|
|
599
|
-
type: 'binary',
|
|
600
|
-
size: 1024,
|
|
601
|
-
}),
|
|
602
|
-
});
|
|
603
|
-
let context = createExecutionContext();
|
|
604
|
-
let response = await worker.fetch(request, testEnv, context);
|
|
605
|
-
await waitOnExecutionContext(context);
|
|
606
|
-
expect(response.status).toBe(400);
|
|
607
|
-
|
|
608
|
-
// Missing type
|
|
609
|
-
request = new Request('http://example.com/resources/presigned-link', {
|
|
610
|
-
method: 'POST',
|
|
611
|
-
headers: {
|
|
612
|
-
'Content-Type': 'application/json',
|
|
613
|
-
},
|
|
614
|
-
body: JSON.stringify({
|
|
615
|
-
key: 'test.txt',
|
|
616
|
-
size: 1024,
|
|
617
|
-
}),
|
|
618
|
-
});
|
|
619
|
-
context = createExecutionContext();
|
|
620
|
-
response = await worker.fetch(request, testEnv, context);
|
|
621
|
-
await waitOnExecutionContext(context);
|
|
622
|
-
expect(response.status).toBe(400);
|
|
623
|
-
|
|
624
|
-
// Missing size
|
|
625
|
-
request = new Request('http://example.com/resources/presigned-link', {
|
|
626
|
-
method: 'POST',
|
|
627
|
-
headers: {
|
|
628
|
-
'Content-Type': 'application/json',
|
|
629
|
-
},
|
|
630
|
-
body: JSON.stringify({
|
|
631
|
-
key: 'test.txt',
|
|
632
|
-
type: 'text',
|
|
633
|
-
}),
|
|
634
|
-
});
|
|
635
|
-
context = createExecutionContext();
|
|
636
|
-
response = await worker.fetch(request, testEnv, context);
|
|
637
|
-
await waitOnExecutionContext(context);
|
|
638
|
-
expect(response.status).toBe(400);
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
it('should validate type field is text or binary', async () => {
|
|
642
|
-
const request = new Request(
|
|
643
|
-
'http://example.com/resources/presigned-link',
|
|
644
|
-
{
|
|
645
|
-
method: 'POST',
|
|
646
|
-
headers: {
|
|
647
|
-
'Content-Type': 'application/json',
|
|
648
|
-
},
|
|
649
|
-
body: JSON.stringify({
|
|
650
|
-
key: 'test.txt',
|
|
651
|
-
type: 'invalid',
|
|
652
|
-
size: 1024,
|
|
653
|
-
}),
|
|
654
|
-
}
|
|
655
|
-
);
|
|
656
|
-
const context = createExecutionContext();
|
|
657
|
-
const response = await worker.fetch(request, testEnv, context);
|
|
658
|
-
await waitOnExecutionContext(context);
|
|
659
|
-
|
|
660
|
-
expect(response.status).toBe(400);
|
|
661
|
-
const responseBody = await response.json<{ error: string }>();
|
|
662
|
-
expect(responseBody.error).toContain(
|
|
663
|
-
'type must be either "text" or "binary"'
|
|
664
|
-
);
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
// Note: We intentionally do not test successful presigned URL generation
|
|
668
|
-
// as it would require real cloud storage credentials and internet connectivity.
|
|
669
|
-
// The spec test validates the API contract when credentials are configured.
|
|
670
|
-
});
|
|
671
|
-
});
|