Skip to content
File

Blob: src/workerd/api/tests/r2-test.js

javascript959 lines
1// Copyright (c) 2023 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 
5import assert from 'node:assert';
6 
7const key = 'basicKey';
8const body = 'content';
9const httpMetaObj = {
10 contentType: 'text/plain',
11 contentLanguage: 'en-US',
12 contentDisposition: 'attachment; filename = "basicKey.txt"',
13 contentEncoding: 'utf-8',
14 cacheControl: 'no-store',
15 cacheExpiry: new Date(1e3),
16};
17const httpFields = {
18 ...httpMetaObj,
19 cacheExpiry: '1000',
20};
21const httpMetaHeaders = new Headers({
22 'content-type': httpMetaObj.contentType,
23 'content-language': httpMetaObj.contentLanguage,
24 'content-disposition': httpMetaObj.contentDisposition,
25 'content-encoding': httpMetaObj.contentEncoding,
26 'cache-control': httpMetaObj.cacheControl,
27 expires: httpMetaObj.cacheExpiry.toUTCString(),
28});
29const customMetadata = {
30 foo: 'bar',
31 baz: 'qux',
32};
33const customFields = Object.entries(customMetadata).map(([k, v]) => ({ k, v }));
34const bufferKey = new Uint8Array([
35 185, 255, 145, 154, 120, 76, 122, 72, 191, 42, 8, 64, 86, 189, 185, 75, 105,
36 37, 155, 123, 165, 158, 4, 42, 222, 13, 135, 52, 87, 154, 181, 227,
37]);
38const hexKey =
39 'b9ff919a784c7a48bf2a084056bdb94b69259b7ba59e042ade0d8734579ab5e3';
40const keyMd5 = 'WGR5pEm07DroP3hYRAh8Yw==';
41const conditionalDate = '946684800000';
42 
43// Test checksums - known values for testing
44const md5Buffer = new Uint8Array([
45 0x9a, 0x03, 0x64, 0xb9, 0xe9, 0x9b, 0xb4, 0x80, 0xdd, 0x25, 0xe1, 0xf0, 0x28,
46 0x4c, 0x85, 0x55,
47]);
48// Test SHA1 checksum
49const sha1Buffer = new Uint8Array([
50 0x2a, 0x03, 0x64, 0xb9, 0xe9, 0x9b, 0xb4, 0x80, 0xdd, 0x25, 0xe1, 0xf0, 0x28,
51 0x4c, 0x85, 0x55, 0x11, 0x22, 0x33, 0x44,
52]);
53// Test SHA256 checksum
54const sha256Buffer = new Uint8Array([
55 0x3a, 0x03, 0x64, 0xb9, 0xe9, 0x9b, 0xb4, 0x80, 0xdd, 0x25, 0xe1, 0xf0, 0x28,
56 0x4c, 0x85, 0x55, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa,
57 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00,
58]);
59const objResponse = {
60 name: key,
61 version: 'objectVersion',
62 size: '123',
63 etag: 'objectEtag',
64 uploaded: '1724767257918',
65 storageClass: 'Standard',
66};
67const HeadObject = {
68 ssecKeyMd5: undefined,
69 storageClass: 'Standard',
70 range: undefined,
71 customMetadata: {},
72 httpMetadata: {},
73 uploaded: new Date(Number(objResponse.uploaded)),
74 checksums: {
75 sha512: undefined,
76 sha384: undefined,
77 sha256: undefined,
78 sha1: undefined,
79 md5: undefined,
80 },
81 httpEtag: '"objectEtag"',
82 etag: 'objectEtag',
83 size: 123,
84 version: 'objectVersion',
85 key,
86};
87 
88function buildGetResponse({ head, body, isList } = {}) {
89 const encoder = new TextEncoder();
90 let meta;
91 if (!isList) {
92 meta = {
93 ...objResponse,
94 };
95 }
96 meta = {
97 ...meta,
98 ...head,
99 };
100 const metadata = encoder.encode(JSON.stringify(meta));
101 const responseBody = body
102 ? new ReadableStream({
103 start(controller) {
104 controller.enqueue(metadata);
105 controller.enqueue(encoder.encode(body));
106 controller.close();
107 },
108 })
109 : metadata;
110 return new Response(responseBody, {
111 headers: {
112 'cf-r2-metadata-size': metadata.length.toString(),
113 'content-length': metadata.length.toString(),
114 },
115 });
116}
117async function compareResponse(res, { head, body } = {}, bytes) {
118 // Destructuring syntax looks ugly, but gets around needing to construct HeadResponse objects(somehow?)
119 const { ...obj } = await res;
120 obj.checksums = { ...obj.checksums };
121 assert.deepEqual(obj, {
122 ...HeadObject,
123 ...head,
124 });
125 if (body) {
126 if (bytes) {
127 const expected = new Uint8Array(new TextEncoder().encode(body));
128 const actual = await (await res).bytes();
129 assert.equal(expected.byteLength, actual.byteLength);
130 for (let i = 0; i < expected.byteLength; i++) {
131 assert.equal(expected[i], actual[i]);
132 }
133 return;
134 }
135 assert.strictEqual(await (await res).text(), body);
136 }
137}
138 
139export default {
140 // Handler for HTTP request binding makes to R2
141 async fetch(request, env, ctx) {
142 // We only expect PUT/Get
143 assert(['GET', 'PUT'].includes(request.method));
144 
145 switch (request.method) {
146 case 'PUT': {
147 // Each request should have a metadata size header indicating how much
148 // we should read to understand what type of request this is
149 const metadataSizeString = request.headers.get('cf-r2-metadata-size');
150 assert.notStrictEqual(metadataSizeString, null);
151 
152 const metadataSize = parseInt(metadataSizeString);
153 assert(!Number.isNaN(metadataSize));
154 
155 const reader = request.body.getReader({ mode: 'byob' });
156 const jsonArray = new Uint8Array(metadataSize);
157 const { value } = await reader.readAtLeast(metadataSize, jsonArray);
158 reader.releaseLock();
159 
160 const jsonRequest = JSON.parse(new TextDecoder().decode(value));
161 
162 // Currently not using the body in these test so I'm going to just discard
163 for await (const _ of request.body) {
164 // intentionally empty
165 }
166 
167 // Assert it's the correct version
168 assert((jsonRequest.version = 1));
169 
170 if (jsonRequest.method === 'delete') {
171 if (jsonRequest.objects) {
172 assert.deepEqual(jsonRequest.objects, [key, key + '2']);
173 } else {
174 assert.deepEqual(jsonRequest.object, key);
175 }
176 return new Response();
177 }
178 
179 switch (jsonRequest.object) {
180 case 'basicKey': {
181 switch (jsonRequest.method) {
182 case 'put': {
183 break;
184 }
185 case 'createMultipartUpload': {
186 return Response.json({
187 uploadId: 'multipartId',
188 });
189 }
190 case 'uploadPart': {
191 return Response.json({
192 etag: 'partEtag',
193 });
194 }
195 case 'abortMultipartUpload': {
196 return new Response();
197 }
198 case 'completeMultipartUpload': {
199 return Response.json(objResponse);
200 }
201 }
202 break;
203 }
204 case 'onlyIfStrongEtag': {
205 assert.deepStrictEqual(jsonRequest.onlyIf, {
206 etagMatches: [
207 {
208 value: 'strongEtag',
209 type: 'strong',
210 },
211 ],
212 etagDoesNotMatch: [
213 {
214 value: 'strongEtag',
215 type: 'strong',
216 },
217 ],
218 uploadedBefore: conditionalDate,
219 });
220 break;
221 }
222 case 'onlyIfWildcard': {
223 assert.deepStrictEqual(jsonRequest.onlyIf, {
224 etagMatches: [
225 {
226 type: 'wildcard',
227 },
228 ],
229 etagDoesNotMatch: [
230 {
231 type: 'wildcard',
232 },
233 ],
234 uploadedAfter: conditionalDate,
235 });
236 break;
237 }
238 case 'httpMetadata': {
239 if (jsonRequest.method !== 'completeMultipartUpload') {
240 assert.deepEqual(jsonRequest.httpFields, httpFields);
241 }
242 const head = {
243 ...objResponse,
244 httpFields,
245 };
246 switch (jsonRequest.method) {
247 case 'put':
248 return Response.json(head);
249 case 'createMultipartUpload':
250 return Response.json({
251 uploadId: 'multipartId',
252 });
253 case 'completeMultipartUpload': {
254 return Response.json(head);
255 }
256 }
257 }
258 // falls through
259 case 'customMetadata': {
260 if (jsonRequest.method !== 'completeMultipartUpload') {
261 assert.deepEqual(jsonRequest.customFields, customFields);
262 }
263 const head = {
264 ...objResponse,
265 customFields,
266 };
267 switch (jsonRequest.method) {
268 case 'put':
269 return Response.json(head);
270 case 'createMultipartUpload':
271 return Response.json({
272 uploadId: 'multipartId',
273 });
274 case 'completeMultipartUpload':
275 return Response.json(head);
276 }
277 }
278 // falls through
279 case 'classDefault': {
280 if (jsonRequest.method !== 'completeMultipartUpload') {
281 assert.strictEqual(jsonRequest.storageClass, undefined);
282 }
283 const head = objResponse;
284 switch (jsonRequest.method) {
285 case 'put':
286 return Response.json(head);
287 case 'createMultipartUpload':
288 return Response.json({
289 uploadId: 'multipartId',
290 });
291 case 'completeMultipartUpload':
292 return Response.json(head);
293 }
294 }
295 // falls through
296 case 'classStandard': {
297 if (jsonRequest.method !== 'completeMultipartUpload') {
298 assert.deepEqual(jsonRequest.storageClass, 'Standard');
299 }
300 const head = {
301 ...objResponse,
302 storageClass: 'Standard',
303 };
304 switch (jsonRequest.method) {
305 case 'put':
306 return Response.json(head);
307 case 'createMultipartUpload':
308 return Response.json({
309 uploadId: 'multipartId',
310 });
311 case 'completeMultipartUpload':
312 return Response.json(head);
313 }
314 }
315 // falls through
316 case 'classInfrequentAccess': {
317 if (jsonRequest.method !== 'completeMultipartUpload') {
318 assert.deepEqual(jsonRequest.storageClass, 'InfrequentAccess');
319 }
320 const head = {
321 ...objResponse,
322 storageClass: 'InfrequentAccess',
323 };
324 switch (jsonRequest.method) {
325 case 'put':
326 return Response.json(head);
327 case 'createMultipartUpload':
328 return Response.json({
329 uploadId: 'multipartId',
330 });
331 case 'completeMultipartUpload':
332 return Response.json(head);
333 }
334 }
335 // falls through
336 case 'ssec': {
337 assert.deepStrictEqual(jsonRequest.ssec, {
338 key: hexKey,
339 });
340 return Response.json({
341 ...objResponse,
342 ssec: {
343 algorithm: 'aes256',
344 keyMd5,
345 },
346 });
347 }
348 case 'ssecMultipart': {
349 if (jsonRequest.method === 'createMultipartUpload') {
350 assert.deepStrictEqual(jsonRequest.ssec, {
351 key: hexKey,
352 });
353 return Response.json({
354 uploadId: 'multipartId',
355 });
356 }
357 if (jsonRequest.method === 'uploadPart') {
358 assert.deepStrictEqual(jsonRequest.ssec, {
359 key: hexKey,
360 });
361 return Response.json({
362 etag: 'partEtag',
363 ssec: {
364 algorithm: 'aes256',
365 keyMd5,
366 },
367 });
368 }
369 if (jsonRequest.method === 'completeMultipartUpload') {
370 return Response.json({
371 ...objResponse,
372 ssec: {
373 algorithm: 'aes256',
374 keyMd5,
375 },
376 });
377 }
378 }
379 // falls through
380 case 'multipleChecksums': {
381 const toHex = (buffer) =>
382 Array.from(buffer, (b) => b.toString(16).padStart(2, '0')).join(
383 ''
384 );
385 return Response.json({
386 ...objResponse,
387 checksums: {
388 0: toHex(md5Buffer), // md5
389 1: toHex(sha1Buffer), // sha1
390 2: toHex(sha256Buffer), // sha256
391 },
392 });
393 }
394 }
395 return Response.json(objResponse);
396 }
397 case 'GET': {
398 const rawHeader = request.headers.get('cf-r2-request');
399 const jsonRequest = JSON.parse(rawHeader);
400 assert((jsonRequest.version = 1));
401 if (jsonRequest.method === 'list') {
402 switch (jsonRequest.prefix) {
403 case 'basic': {
404 assert.deepEqual(jsonRequest, {
405 cursor: 'ai',
406 delimiter: '/',
407 include: [0, 1],
408 limit: 1,
409 method: 'list',
410 newRuntime: true,
411 prefix: 'basic',
412 version: 1,
413 });
414 return buildGetResponse({
415 head: {
416 objects: [objResponse],
417 truncated: true,
418 cursor: 'ai',
419 deliminatedPrefixes: [],
420 },
421 isList: true,
422 });
423 }
424 case 'httpMeta': {
425 assert.deepEqual(jsonRequest, {
426 include: [0],
427 method: 'list',
428 newRuntime: true,
429 prefix: 'httpMeta',
430 version: 1,
431 });
432 
433 return buildGetResponse({
434 head: {
435 objects: [{ ...objResponse, httpFields, customFields: [] }],
436 truncated: false,
437 deliminatedPrefixes: [],
438 },
439 isList: true,
440 });
441 }
442 case 'customMeta': {
443 assert.deepEqual(jsonRequest, {
444 include: [1],
445 method: 'list',
446 newRuntime: true,
447 prefix: 'customMeta',
448 version: 1,
449 });
450 
451 return buildGetResponse({
452 head: {
453 objects: [{ ...objResponse, httpFields: {}, customFields }],
454 truncated: false,
455 deliminatedPrefixes: [],
456 },
457 isList: true,
458 });
459 }
460 }
461 }
462 assert(['get', 'head'].includes(jsonRequest.method));
463 switch (jsonRequest.object) {
464 case 'basicKey': {
465 return buildGetResponse({ body });
466 }
467 case 'rangeOffLen': {
468 assert.deepEqual(jsonRequest.range, {
469 offset: '1',
470 length: '3',
471 });
472 return buildGetResponse({
473 head: {
474 range: jsonRequest.range,
475 },
476 body: 'ont',
477 });
478 }
479 case 'rangeSuff': {
480 assert.deepEqual(jsonRequest.range, {
481 suffix: '2',
482 });
483 return buildGetResponse({
484 head: {
485 range: {
486 offset: '6',
487 length: '2',
488 },
489 },
490 body: 'nt',
491 });
492 }
493 case 'onlyIfStrongEtag': {
494 assert.deepStrictEqual(jsonRequest.onlyIf, {
495 etagMatches: [
496 {
497 value: 'strongEtag',
498 type: 'strong',
499 },
500 ],
501 etagDoesNotMatch: [
502 {
503 value: 'strongEtag',
504 type: 'strong',
505 },
506 ],
507 uploadedBefore: conditionalDate,
508 });
509 return buildGetResponse({ body });
510 }
511 case 'onlyIfWildcard': {
512 assert.deepStrictEqual(jsonRequest.onlyIf, {
513 etagMatches: [
514 {
515 type: 'wildcard',
516 },
517 ],
518 etagDoesNotMatch: [
519 {
520 type: 'wildcard',
521 },
522 ],
523 uploadedAfter: conditionalDate,
524 });
525 return buildGetResponse({ body });
526 }
527 case 'httpMetadata': {
528 const head = {
529 httpFields,
530 };
531 switch (jsonRequest.method) {
532 case 'head':
533 return buildGetResponse({ head });
534 case 'get':
535 return buildGetResponse({ head, body });
536 }
537 }
538 // falls through
539 case 'customMetadata': {
540 const head = {
541 customFields,
542 };
543 switch (jsonRequest.method) {
544 case 'head':
545 return buildGetResponse({ head });
546 case 'get':
547 return buildGetResponse({ head, body });
548 }
549 }
550 // falls through
551 case 'classDefault':
552 // falls through
553 case 'classStandard': {
554 const head = {
555 storageClass: 'Standard',
556 };
557 switch (jsonRequest.method) {
558 case 'head':
559 return buildGetResponse({ head });
560 case 'get':
561 return buildGetResponse({ head, body });
562 }
563 }
564 // falls through
565 case 'classInfrequentAccess': {
566 const head = {
567 storageClass: 'InfrequentAccess',
568 };
569 switch (jsonRequest.method) {
570 case 'head':
571 return buildGetResponse({ head });
572 case 'get':
573 return buildGetResponse({ head, body });
574 }
575 }
576 // falls through
577 case 'ssec': {
578 return buildGetResponse({
579 head: {
580 ssec: {
581 algorithm: 'aes256',
582 keyMd5,
583 },
584 },
585 body,
586 });
587 }
588 case 'multipleChecksums': {
589 const toHex = (buffer) =>
590 Array.from(buffer, (b) => b.toString(16).padStart(2, '0')).join(
591 ''
592 );
593 return buildGetResponse({
594 head: {
595 checksums: {
596 0: toHex(md5Buffer), // md5
597 1: toHex(sha1Buffer), // sha1
598 2: toHex(sha256Buffer), // sha256
599 },
600 },
601 body: jsonRequest.method === 'get' ? body : undefined,
602 });
603 }
604 }
605 throw new Error('Unexpected GET');
606 }
607 default:
608 throw new Error('Unexpected HTTP Method');
609 }
610 },
611 async test(ctrl, env, ctx) {
612 // Basic Operations
613 {
614 // PutObject
615 await compareResponse(env.BUCKET.put(key, body));
616 // GetObject
617 await compareResponse(env.BUCKET.get(key), {
618 body,
619 });
620 // GetObject(.bytes())
621 await compareResponse(env.BUCKET.get(key), { body }, true);
622 // HeadObject
623 await compareResponse(env.BUCKET.head(key));
624 // MultipartUploads
625 {
626 // CreateMultipartUpload
627 const multi = await env.BUCKET.createMultipartUpload(key);
628 assert.equal(multi.uploadId, 'multipartId');
629 assert.equal(multi.key, key);
630 // UploadPart
631 const part = await multi.uploadPart(1, body);
632 assert.equal(part.etag, 'partEtag');
633 // Abort(doesn't quite make sense to abort **and** complete, but shouldn't matter)
634 await multi.abort();
635 // CompleteMultipartUpload
636 await compareResponse(
637 multi.complete([
638 {
639 partNumber: 1,
640 etag: 'partEtag',
641 },
642 ])
643 );
644 }
645 // ListObjects
646 {
647 const list = await env.BUCKET.list({
648 limit: 1,
649 prefix: 'basic',
650 cursor: 'ai',
651 delimiter: '/',
652 include: ['httpMetadata', 'customMetadata'],
653 });
654 list.objects[0] = { ...list.objects[0] };
655 list.objects[0].checksums = { ...list.objects[0].checksums };
656 assert.deepEqual(list, {
657 objects: [HeadObject],
658 truncated: true,
659 cursor: 'ai',
660 delimitedPrefixes: [],
661 });
662 }
663 // DeleteObject
664 {
665 await env.BUCKET.delete(key);
666 await env.BUCKET.delete([key, 'basicKey2']);
667 }
668 }
669 // Ranged Reads
670 {
671 // Offset/Length
672 {
673 const range = {
674 offset: 1,
675 length: 3,
676 };
677 await compareResponse(
678 env.BUCKET.get('rangeOffLen', {
679 range,
680 }),
681 {
682 head: { range },
683 },
684 'ont'
685 );
686 }
687 // Suffix
688 await compareResponse(
689 env.BUCKET.get('rangeSuff', {
690 range: {
691 suffix: 2,
692 },
693 }),
694 {
695 head: {
696 range: {
697 offset: 6,
698 length: 2,
699 },
700 },
701 },
702 'nt'
703 );
704 }
705 // Conditionals
706 {
707 try {
708 await env.BUCKET.put('throwOnInvalidEtag', body, {
709 onlyIf: new Headers({
710 'if-match': 'strongEtag',
711 }),
712 });
713 throw new Error('This should have thrown');
714 } catch {
715 // intentionally empty
716 }
717 try {
718 await env.BUCKET.put('throwOnInvalidEtag', body, {
719 onlyIf: new Headers({
720 'if-none-match': 'strongEtag',
721 }),
722 });
723 throw new Error('This should have thrown');
724 } catch {
725 // intentionally empty
726 }
727 await env.BUCKET.put('onlyIfStrongEtag', body, {
728 onlyIf: {
729 etagMatches: 'strongEtag',
730 etagDoesNotMatch: 'strongEtag',
731 uploadedBefore: new Date('0'),
732 },
733 });
734 await env.BUCKET.put('onlyIfWildcard', body, {
735 onlyIf: {
736 etagMatches: '*',
737 etagDoesNotMatch: '*',
738 uploadedAfter: new Date('0'),
739 },
740 });
741 await env.BUCKET.get('onlyIfStrongEtag', {
742 onlyIf: {
743 etagMatches: 'strongEtag',
744 etagDoesNotMatch: 'strongEtag',
745 uploadedBefore: new Date('0'),
746 },
747 });
748 await env.BUCKET.get('onlyIfWildcard', {
749 onlyIf: {
750 etagMatches: '*',
751 etagDoesNotMatch: '*',
752 uploadedAfter: new Date('0'),
753 },
754 });
755 }
756 // Metadata
757 {
758 // httpMetadata
759 for (const httpMetadata of [httpMetaObj, httpMetaHeaders]) {
760 const head = {
761 httpMetadata: httpMetaObj,
762 };
763 // PutObject
764 await compareResponse(
765 env.BUCKET.put('httpMetadata', body, {
766 httpMetadata,
767 }),
768 { head }
769 );
770 // HeadObject
771 await compareResponse(env.BUCKET.head('httpMetadata'), { head });
772 // GetObject
773 {
774 const objWithBody = await env.BUCKET.get('httpMetadata');
775 await compareResponse(objWithBody, { head, body });
776 // Hijacking this test to test `writeHttpMetadata` too
777 const createdHeaders = new Headers();
778 objWithBody.writeHttpMetadata(createdHeaders);
779 assert.deepEqual(
780 Object.fromEntries(createdHeaders.entries()),
781 Object.fromEntries(httpMetaHeaders.entries())
782 );
783 }
784 // ListObjects
785 {
786 const list = await env.BUCKET.list({
787 prefix: 'httpMeta',
788 include: ['httpMetadata'],
789 });
790 list.objects[0] = { ...list.objects[0] };
791 list.objects[0].checksums = { ...list.objects[0].checksums };
792 assert.deepEqual(list, {
793 delimitedPrefixes: [],
794 objects: [{ ...HeadObject, ...head }],
795 truncated: false,
796 });
797 }
798 // Multipart Upload
799 await compareResponse(
800 (
801 await env.BUCKET.createMultipartUpload('httpMetadata', {
802 httpMetadata,
803 })
804 ).complete([]),
805 { head }
806 );
807 }
808 // customMetadata
809 {
810 // PutObject
811 const head = {
812 customMetadata,
813 };
814 await compareResponse(
815 env.BUCKET.put('customMetadata', body, {
816 customMetadata,
817 }),
818 { head }
819 );
820 // HeadObject
821 await compareResponse(await env.BUCKET.head('customMetadata'), {
822 head,
823 });
824 // GetObject
825 await compareResponse(await env.BUCKET.get('customMetadata'), {
826 head,
827 body,
828 });
829 // ListObjects
830 {
831 const list = await env.BUCKET.list({
832 prefix: 'customMeta',
833 include: ['customMetadata'],
834 });
835 list.objects[0] = { ...list.objects[0] };
836 list.objects[0].checksums = { ...list.objects[0].checksums };
837 assert.deepEqual(list, {
838 delimitedPrefixes: [],
839 objects: [{ ...HeadObject, ...head }],
840 truncated: false,
841 });
842 }
843 // Multipart Upload
844 await compareResponse(
845 (
846 await env.BUCKET.createMultipartUpload('customMetadata', {
847 customMetadata,
848 })
849 ).complete([]),
850 { head }
851 );
852 }
853 }
854 // StorageClasses
855 {
856 for (const storageClassName of [
857 'Default',
858 'Standard',
859 'InfrequentAccess',
860 ]) {
861 const key = 'class' + storageClassName;
862 const storageClass =
863 storageClassName === 'Default' ? undefined : storageClassName;
864 const head = {
865 storageClass:
866 storageClassName === 'Default' ? 'Standard' : storageClassName,
867 };
868 // PutObject
869 await compareResponse(
870 env.BUCKET.put(key, body, {
871 storageClass,
872 }),
873 { head }
874 );
875 // HeadObject
876 await compareResponse(env.BUCKET.head(key), { head });
877 // GetObject
878 await compareResponse(env.BUCKET.get(key), { head, body });
879 // Multipart Upload
880 await compareResponse(
881 (
882 await env.BUCKET.createMultipartUpload(key, {
883 storageClass,
884 })
885 ).complete([]),
886 { head }
887 );
888 }
889 }
890 // SSEC
891 {
892 const head = {
893 ssecKeyMd5: keyMd5,
894 };
895 for (const ssecKey of [bufferKey, hexKey]) {
896 // PutObject
897 await compareResponse(
898 env.BUCKET.put('ssec', body, {
899 ssecKey,
900 }),
901 { head }
902 );
903 // GetObject
904 await compareResponse(
905 env.BUCKET.get('ssec', {
906 ssecKey,
907 }),
908 { head, body }
909 );
910 // HeadObject
911 await compareResponse(
912 env.BUCKET.head('ssec', {
913 ssecKey,
914 }),
915 { head }
916 );
917 // MultipartUpload
918 {
919 // CreateMultipartUpload
920 const multi = await env.BUCKET.createMultipartUpload(
921 'ssecMultipart',
922 {
923 ssecKey,
924 }
925 );
926 assert.equal(multi.uploadId, 'multipartId');
927 // UploadPart
928 const part = await multi.uploadPart(1, 'hey', {
929 ssecKey,
930 });
931 assert.equal(part.etag, 'partEtag');
932 // CompleteMultipartUpload
933 await compareResponse(
934 multi.complete([
935 {
936 partNumber: 1,
937 etag: 'partEtag',
938 },
939 ]),
940 { head }
941 );
942 }
943 }
944 }
945 // Checksums
946 {
947 // This tests the instrumentation with multiple checksums to ensure proper tag handling
948 let resp = await env.BUCKET.put('multipleChecksums', body, {
949 md5: md5Buffer,
950 });
951 assert.ok(resp);
952 
953 // Also test HEAD operation to verify checksum tags
954 const headResp = await env.BUCKET.head('multipleChecksums');
955 assert.ok(headResp);
956 }
957 },
958};