Skip to content
File

Blob: src/cloudflare/internal/test/images/images-api-test.js

javascript690 lines
1// Copyright (c) 2024 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4// @ts-expect-error - no type declarations for node:assert in this context
5import * as assert from 'node:assert';
6 
7const encoder = new TextEncoder();
8function inputStream(chunks) {
9 return new ReadableStream({
10 start(controller) {
11 for (const chunk of chunks) {
12 controller.enqueue(encoder.encode(chunk));
13 }
14 controller.close();
15 },
16 });
17}
18 
19/**
20 * @typedef {{'images': ImagesBinding}} Env
21 *
22 */
23 
24/**
25 * @param {Env} env
26 * @param {string[]} chunks
27 * @returns {Promise<string>}
28 */
29async function decodeBase64ThroughImagesBinding(env, chunks) {
30 const result = await env.images
31 .input(inputStream(chunks), { encoding: 'base64' })
32 .output({ format: 'image/avif' });
33 
34 const body = await result.response().json();
35 return body.image;
36}
37 
38export const test_images_info_bitmap = {
39 /**
40 * @param {unknown} _
41 * @param {Env} env
42 */
43 async test(_, env) {
44 const info = await env.images.info(inputStream(['png']));
45 assert.deepStrictEqual(info, {
46 format: 'image/png',
47 fileSize: 123,
48 width: 123,
49 height: 123,
50 });
51 },
52};
53 
54export const test_images_info_bitmap_base64 = {
55 /**
56 * @param {unknown} _
57 * @param {Env} env
58 */
59 async test(_, env) {
60 const info = await env.images.info(inputStream([btoa('png')]), {
61 encoding: 'base64',
62 });
63 assert.deepStrictEqual(info, {
64 format: 'image/png',
65 fileSize: 123,
66 width: 123,
67 height: 123,
68 });
69 },
70};
71 
72export const test_images_info_svg = {
73 /**
74 * @param {unknown} _
75 * @param {Env} env
76 */
77 async test(_, env) {
78 const info = await env.images.info(inputStream(['<svg></svg>']));
79 assert.deepStrictEqual(info, {
80 format: 'image/svg+xml',
81 });
82 },
83};
84 
85export const test_images_info_error = {
86 /**
87 * @param {unknown} _
88 * @param {Env} env
89 */
90 async test(_, env) {
91 /**
92 * @type {any} e;
93 */
94 let e;
95 
96 try {
97 await env.images.info(inputStream(['BAD']));
98 } catch (e2) {
99 e = e2;
100 }
101 
102 assert.equal(true, !!e);
103 assert.equal(e.code, 123);
104 assert.equal(e.message, 'IMAGES_INFO_ERROR 123: Bad request');
105 },
106};
107 
108export const test_images_transform = {
109 /**
110 * @param {unknown} _
111 * @param {Env} env
112 */
113 
114 async test(_, env) {
115 const result = await env.images
116 .input(inputStream(['png']))
117 .transform({ rotate: 90 })
118 .output({ format: 'image/avif', anim: true });
119 
120 // Would be image/avif in real life, but mock always returns JSON
121 assert.strictEqual(result.contentType(), 'application/json');
122 const body = await result.response().json();
123 
124 assert.deepStrictEqual(body, {
125 image: 'png',
126 output_format: 'image/avif',
127 transforms: [{ imageIndex: 0, rotate: 90 }],
128 anim: 'true',
129 });
130 },
131};
132 
133export const test_images_nested_draw = {
134 /**
135 * @param {unknown} _
136 * @param {Env} env
137 */
138 
139 async test(_, env) {
140 const result = await env.images
141 .input(inputStream(['png']))
142 .transform({ rotate: 90 })
143 .draw(env.images.input(inputStream(['png1'])).transform({ rotate: 180 }))
144 .draw(
145 env.images
146 .input(inputStream(['png2']))
147 .draw(inputStream(['png3']))
148 .transform({ rotate: 270 })
149 )
150 .draw(inputStream(['png4']))
151 .output({ format: 'image/avif' });
152 
153 // Would be image/avif in real life, but mock always returns JSON
154 assert.strictEqual(result.contentType(), 'application/json');
155 const body = await result.response().json();
156 
157 assert.deepStrictEqual(body, {
158 image: 'png',
159 draw_image: ['png1', 'png2', 'png3', 'png4'],
160 output_format: 'image/avif',
161 transforms: [
162 { imageIndex: 0, rotate: 90 },
163 { imageIndex: 1, rotate: 180 },
164 { drawImageIndex: 1, targetImageIndex: 0 },
165 { drawImageIndex: 3, targetImageIndex: 2 },
166 { imageIndex: 2, rotate: 270 },
167 { drawImageIndex: 2, targetImageIndex: 0 },
168 { drawImageIndex: 4, targetImageIndex: 0 },
169 ],
170 });
171 },
172};
173 
174export const test_images_transformer_draw_twice_disallowed = {
175 /**
176 * @param {unknown} _
177 * @param {Env} env
178 */
179 
180 async test(_, env) {
181 /**
182 * @type {any} e;
183 */
184 let e;
185 
186 let t = env.images.input(inputStream(['png1']));
187 
188 try {
189 await env.images
190 .input(inputStream(['png']))
191 .draw(t)
192 .draw(t)
193 .output({ format: 'image/avif' });
194 } catch (e1) {
195 e = e1;
196 }
197 
198 assert.equal(true, !!e);
199 assert.equal(e.code, 9525);
200 assert.equal(
201 e.message,
202 'IMAGES_TRANSFORM_ERROR 9525: ImageTransformer consumed; you may only call .output() or draw a transformer once'
203 );
204 },
205};
206 
207export const test_images_transformer_already_consumed_disallowed = {
208 /**
209 * @param {unknown} _
210 * @param {Env} env
211 */
212 
213 async test(_, env) {
214 /**
215 * @type {any} e;
216 */
217 let e;
218 
219 let t = env.images.input(inputStream(['png1']));
220 
221 await t.output({});
222 
223 try {
224 await env.images
225 .input(inputStream(['png']))
226 .draw(t)
227 .output({ format: 'image/avif' });
228 } catch (e1) {
229 e = e1;
230 }
231 
232 assert.equal(true, !!e);
233 assert.equal(e.code, 9525);
234 assert.equal(
235 e.message,
236 'IMAGES_TRANSFORM_ERROR 9525: ImageTransformer consumed; you may only call .output() or draw a transformer once'
237 );
238 },
239};
240 
241export const test_images_transform_bad = {
242 /**
243 * @param {unknown} _
244 * @param {Env} env
245 */
246 
247 async test(_, env) {
248 /**
249 * @type {any} e;
250 */
251 let e;
252 
253 try {
254 await env.images
255 .input(inputStream(['BAD']))
256 .transform({ rotate: 90 })
257 .output({ format: 'image/avif' });
258 } catch (e2) {
259 e = e2;
260 }
261 
262 assert.equal(true, !!e);
263 assert.equal(e.code, 123);
264 assert.equal(e.message, 'IMAGES_TRANSFORM_ERROR 123: Bad request');
265 },
266};
267 
268export const test_images_transform_consumed = {
269 /**
270 * @param {unknown} _
271 * @param {Env} env
272 */
273 
274 async test(_, env) {
275 /**
276 * @type {any} e;
277 */
278 let e;
279 
280 try {
281 let transformer = env.images
282 .input(inputStream(['png']))
283 .transform({ rotate: 90 });
284 
285 await transformer.output({ format: 'image/avif' });
286 await transformer.output({ format: 'image/avif' });
287 } catch (e2) {
288 e = e2;
289 }
290 
291 assert.equal(true, !!e);
292 assert.equal(e.code, 9525);
293 assert.equal(
294 e.message,
295 'IMAGES_TRANSFORM_ERROR 9525: ImageTransformer consumed; you may only call .output() or draw a transformer once'
296 );
297 },
298};
299 
300const ENCODE_BLOCKSIZE = 32 * 1024 + 1;
301const DECODE_BLOCKSIZE = 32 * 1024;
302 
303export const test_images_base64_input = {
304 /**
305 * @param {unknown} _
306 * @param {Env} env
307 */
308 async test(_, env) {
309 const inputs = {
310 smallInput: 'QUFB',
311 blockSizeInput: 'QUFB'.repeat(DECODE_BLOCKSIZE / 'QUFB'.length),
312 twoBlockInput: 'QUFB'.repeat(DECODE_BLOCKSIZE / 'QUFB'.length + 1),
313 hugeInput: 'QUFB'.repeat((DECODE_BLOCKSIZE * 20) / 'QUFB'.length),
314 };
315 
316 for (let testName in inputs) {
317 const input = inputs[testName];
318 
319 const result = await env.images
320 .input(inputStream([input]), { encoding: 'base64' })
321 .transform({ rotate: 90 })
322 .output({ format: 'image/avif' });
323 
324 // Would be image/avif in real life, but mock always returns JSON
325 assert.strictEqual(result.contentType(), 'application/json');
326 const body = await result.response().json();
327 
328 assert.deepStrictEqual(body, {
329 image: atob(input),
330 output_format: 'image/avif',
331 transforms: [{ imageIndex: 0, rotate: 90 }],
332 });
333 }
334 },
335};
336 
337export const test_images_base64_output = {
338 /**
339 * @param {unknown} _
340 * @param {Env} env
341 */
342 
343 async test(_, env) {
344 // We can't really control the length of the output here as it also has
345 // a JSON body around it
346 const inputs = {
347 smallInput: 'A',
348 twoBlockInput: 'A'.repeat(ENCODE_BLOCKSIZE + 1),
349 hugeInput: 'A'.repeat(ENCODE_BLOCKSIZE * 20),
350 };
351 
352 for (const testName in inputs) {
353 const input = inputs[testName];
354 
355 const result = await env.images
356 .input(inputStream([input]))
357 .transform({ rotate: 90 })
358 .output({ format: 'image/avif' });
359 
360 // Would be image/avif in real life, but mock always returns JSON
361 assert.strictEqual(result.contentType(), 'application/json');
362 const bodyBase64 = await new Response(
363 await result.image({ encoding: 'base64' })
364 ).text();
365 const body = JSON.parse(atob(bodyBase64));
366 
367 assert.deepStrictEqual(body, {
368 image: input,
369 output_format: 'image/avif',
370 transforms: [{ imageIndex: 0, rotate: 90 }],
371 });
372 }
373 },
374};
375 
376export const test_images_base64_empty_input = {
377 /**
378 * @param {unknown} _
379 * @param {Env} env
380 */
381 async test(_, env) {
382 assert.strictEqual(await decodeBase64ThroughImagesBinding(env, []), '');
383 },
384};
385 
386export const test_images_base64_padding = {
387 /**
388 * @param {unknown} _
389 * @param {Env} env
390 */
391 async test(_, env) {
392 assert.strictEqual(
393 await decodeBase64ThroughImagesBinding(env, ['QQ==']),
394 'A'
395 );
396 assert.strictEqual(
397 await decodeBase64ThroughImagesBinding(env, ['QWI=']),
398 'Ab'
399 );
400 assert.strictEqual(
401 await decodeBase64ThroughImagesBinding(env, ['QWJj']),
402 'Abc'
403 );
404 },
405};
406 
407export const test_images_base64_chunk_boundaries = {
408 /**
409 * @param {unknown} _
410 * @param {Env} env
411 */
412 async test(_, env) {
413 assert.strictEqual(
414 await decodeBase64ThroughImagesBinding(env, ['QUFB', 'QUFB']),
415 'AAAAAA'
416 );
417 },
418};
419 
420export const test_images_base64_small_chunks = {
421 /**
422 * @param {unknown} _
423 * @param {Env} env
424 */
425 async test(_, env) {
426 // 1 byte chunks
427 assert.strictEqual(
428 await decodeBase64ThroughImagesBinding(env, [
429 'Q',
430 'U',
431 'F',
432 'B',
433 'Q',
434 'U',
435 'F',
436 'B',
437 ]),
438 'AAAAAA'
439 );
440 
441 // 2 byte chunks
442 assert.strictEqual(
443 await decodeBase64ThroughImagesBinding(env, ['QU', 'FB', 'QU', 'FB']),
444 'AAAAAA'
445 );
446 
447 // 3 byte chunks
448 assert.strictEqual(
449 await decodeBase64ThroughImagesBinding(env, ['QUF', 'BQU', 'FB']),
450 'AAAAAA'
451 );
452 },
453};
454// GET metadata
455export const test_images_get_success = {
456 /**
457 * @param {unknown} _
458 * @param {Env} env
459 */
460 async test(_, env) {
461 const metadata = await env.images.hosted.image('test-image-id').details();
462 assert.notEqual(metadata, null);
463 assert.equal(metadata.id, 'test-image-id');
464 assert.equal(metadata.filename, 'test.jpg');
465 assert.equal(metadata.requireSignedURLs, false);
466 assert.equal(metadata.creator, 'test-creator');
467 },
468};
469 
470export const test_images_get_not_found = {
471 /**
472 * @param {unknown} _
473 * @param {Env} env
474 */
475 async test(_, env) {
476 const metadata = await env.images.hosted.image('not-found').details();
477 assert.equal(metadata, null);
478 },
479};
480 
481// GET image blob
482export const test_images_getImage_success = {
483 /**
484 * @param {unknown} _
485 * @param {Env} env
486 */
487 async test(_, env) {
488 const stream = await env.images.hosted.image('test-image-id').bytes();
489 assert.notEqual(stream, null);
490 
491 const reader = stream.getReader();
492 let result = '';
493 while (true) {
494 const { done, value } = await reader.read();
495 if (done) break;
496 result += new TextDecoder().decode(value);
497 }
498 
499 assert.equal(result, 'MOCK_IMAGE_DATA_test-image-id');
500 },
501};
502 
503export const test_images_getImage_not_found = {
504 /**
505 * @param {unknown} _
506 * @param {Env} env
507 */
508 async test(_, env) {
509 const stream = await env.images.hosted.image('not-found').bytes();
510 assert.equal(stream, null);
511 },
512};
513 
514// UPLOAD
515export const test_images_upload_with_options = {
516 /**
517 * @param {unknown} _
518 * @param {Env} env
519 */
520 async test(_, env) {
521 const imageData = new Blob(['test image']).stream();
522 const metadata = await env.images.hosted.upload(imageData, {
523 id: 'custom-id',
524 filename: 'upload-test.jpg',
525 requireSignedURLs: true,
526 metadata: { key: 'value' },
527 creator: 'upload-creator',
528 });
529 
530 assert.equal(metadata.id, 'custom-id');
531 assert.equal(metadata.filename, 'upload-test.jpg');
532 assert.equal(metadata.requireSignedURLs, true);
533 assert.deepStrictEqual(metadata.meta, { key: 'value' });
534 assert.equal(metadata.creator, 'upload-creator');
535 },
536};
537 
538export const test_images_upload_arraybuffer = {
539 /**
540 * @param {unknown} _
541 * @param {Env} env
542 */
543 async test(_, env) {
544 const buffer = new TextEncoder().encode('test image').buffer;
545 const metadata = await env.images.hosted.upload(buffer);
546 
547 assert.notEqual(metadata, null);
548 assert.equal(typeof metadata.id, 'string');
549 },
550};
551 
552// UPDATE
553export const test_images_update_success = {
554 /**
555 * @param {unknown} _
556 * @param {Env} env
557 */
558 async test(_, env) {
559 const metadata = await env.images.hosted.image('test-image-id').update({
560 requireSignedURLs: true,
561 metadata: { updated: true },
562 creator: 'update-creator',
563 });
564 
565 assert.equal(metadata.id, 'test-image-id');
566 assert.equal(metadata.requireSignedURLs, true);
567 assert.deepStrictEqual(metadata.meta, { updated: true });
568 assert.equal(metadata.creator, 'update-creator');
569 },
570};
571 
572export const test_images_update_not_found = {
573 /**
574 * @param {unknown} _
575 * @param {Env} env
576 */
577 async test(_, env) {
578 /**
579 * @type {any} e;
580 */
581 let e;
582 try {
583 await env.images.hosted
584 .image('not-found')
585 .update({ requireSignedURLs: true });
586 } catch (err) {
587 e = err;
588 }
589 assert.notEqual(e, undefined);
590 assert.equal(e.message.includes('not found'), true);
591 },
592};
593 
594// DELETE
595export const test_images_delete_success = {
596 /**
597 * @param {unknown} _
598 * @param {Env} env
599 */
600 async test(_, env) {
601 const result = await env.images.hosted.image('test-image-id').delete();
602 assert.equal(result, true);
603 },
604};
605 
606export const test_images_delete_not_found = {
607 /**
608 * @param {unknown} _
609 * @param {Env} env
610 */
611 async test(_, env) {
612 const result = await env.images.hosted.image('not-found').delete();
613 assert.equal(result, false);
614 },
615};
616 
617// LIST
618export const test_images_list_default = {
619 /**
620 * @param {unknown} _
621 * @param {Env} env
622 */
623 async test(_, env) {
624 const result = await env.images.hosted.list();
625 
626 assert.notEqual(result.images, null);
627 assert.equal(Array.isArray(result.images), true);
628 assert.equal(result.images.length, 2);
629 assert.equal(result.listComplete, true);
630 },
631};
632 
633export const test_images_list_with_options = {
634 /**
635 * @param {unknown} _
636 * @param {Env} env
637 */
638 async test(_, env) {
639 const result = await env.images.hosted.list({
640 limit: 1,
641 sortOrder: 'asc',
642 });
643 
644 assert.equal(result.images.length, 1);
645 },
646};
647 
648// UPLOAD with base64 encoding
649export const test_images_upload_base64_stream = {
650 /**
651 * @param {unknown} _
652 * @param {Env} env
653 */
654 async test(_, env) {
655 // Create base64-encoded data
656 const imageData = 'test image content';
657 const base64Data = btoa(imageData);
658 const stream = new Blob([base64Data]).stream();
659 
660 const metadata = await env.images.hosted.upload(stream, {
661 filename: 'base64-test.jpg',
662 encoding: 'base64',
663 });
664 
665 assert.equal(metadata.filename, 'base64-test.jpg');
666 assert.notEqual(metadata.id, null);
667 },
668};
669 
670export const test_images_upload_base64_arraybuffer = {
671 /**
672 * @param {unknown} _
673 * @param {Env} env
674 */
675 async test(_, env) {
676 // Create base64-encoded ArrayBuffer
677 const imageData = 'test image content';
678 const base64Data = btoa(imageData);
679 const buffer = new TextEncoder().encode(base64Data).buffer;
680 
681 const metadata = await env.images.hosted.upload(buffer, {
682 filename: 'base64-buffer-test.jpg',
683 encoding: 'base64',
684 });
685 
686 assert.equal(metadata.filename, 'base64-buffer-test.jpg');
687 assert.notEqual(metadata.id, null);
688 },
689};