@@ -5,7 +5,7 @@ import semver from 'semver'
5
5
import { describe , test , expect , beforeAll , afterEach } from 'vitest'
6
6
7
7
import { MockFetch } from '../test/mock_fetch.js'
8
- import { streamToString } from '../test/util.js'
8
+ import { base64Encode , streamToString } from '../test/util.js'
9
9
10
10
import { MissingBlobsEnvironmentError } from './environment.js'
11
11
import { getDeployStore , getStore } from './main.js'
@@ -164,34 +164,6 @@ describe('get', () => {
164
164
165
165
expect ( mockStore . fulfilled ) . toBeTruthy ( )
166
166
} )
167
-
168
- test ( 'Returns `null` when the blob entry contains an expiry date in the past' , async ( ) => {
169
- const mockStore = new MockFetch ( )
170
- . get ( {
171
- headers : { authorization : `Bearer ${ apiToken } ` } ,
172
- response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
173
- url : `https://api.netlify.com/api/v1/sites/${ siteID } /blobs/${ key } ?context=production` ,
174
- } )
175
- . get ( {
176
- response : new Response ( value , {
177
- headers : {
178
- 'x-nf-expires-at' : ( Date . now ( ) - 1000 ) . toString ( ) ,
179
- } ,
180
- } ) ,
181
- url : signedURL ,
182
- } )
183
-
184
- globalThis . fetch = mockStore . fetch
185
-
186
- const blobs = getStore ( {
187
- name : 'production' ,
188
- token : apiToken ,
189
- siteID,
190
- } )
191
-
192
- expect ( await blobs . get ( key ) ) . toBeNull ( )
193
- expect ( mockStore . fulfilled ) . toBeTruthy ( )
194
- } )
195
167
} )
196
168
197
169
describe ( 'With edge credentials' , ( ) => {
@@ -318,6 +290,162 @@ describe('get', () => {
318
290
} )
319
291
} )
320
292
293
+ describe ( 'getWithMetadata' , ( ) => {
294
+ describe ( 'With API credentials' , ( ) => {
295
+ test ( 'Reads from the blob store and returns the etag and the metadata object' , async ( ) => {
296
+ const mockMetadata = {
297
+ name : 'Netlify' ,
298
+ cool : true ,
299
+ functions : [ 'edge' , 'serverless' ] ,
300
+ }
301
+ const responseHeaders = {
302
+ etag : '123456789' ,
303
+ 'x-amz-meta-user' : `b64;${ base64Encode ( mockMetadata ) } ` ,
304
+ }
305
+ const mockStore = new MockFetch ( )
306
+ . get ( {
307
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
308
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
309
+ url : `https://api.netlify.com/api/v1/sites/${ siteID } /blobs/${ key } ?context=production` ,
310
+ } )
311
+ . get ( {
312
+ response : new Response ( value , { headers : responseHeaders } ) ,
313
+ url : signedURL ,
314
+ } )
315
+ . get ( {
316
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
317
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
318
+ url : `https://api.netlify.com/api/v1/sites/${ siteID } /blobs/${ key } ?context=production` ,
319
+ } )
320
+ . get ( {
321
+ response : new Response ( value , { headers : responseHeaders } ) ,
322
+ url : signedURL ,
323
+ } )
324
+
325
+ globalThis . fetch = mockStore . fetch
326
+
327
+ const blobs = getStore ( {
328
+ name : 'production' ,
329
+ token : apiToken ,
330
+ siteID,
331
+ } )
332
+
333
+ const entry1 = await blobs . getWithMetadata ( key )
334
+ expect ( entry1 . data ) . toBe ( value )
335
+ expect ( entry1 . etag ) . toBe ( responseHeaders . etag )
336
+ expect ( entry1 . metadata ) . toEqual ( mockMetadata )
337
+
338
+ const entry2 = await blobs . getWithMetadata ( key , { type : 'stream' } )
339
+ expect ( await streamToString ( entry2 . data as unknown as NodeJS . ReadableStream ) ) . toBe ( value )
340
+ expect ( entry2 . etag ) . toBe ( responseHeaders . etag )
341
+ expect ( entry2 . metadata ) . toEqual ( mockMetadata )
342
+
343
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
344
+ } )
345
+
346
+ test ( 'Returns `null` when the pre-signed URL returns a 404' , async ( ) => {
347
+ const mockStore = new MockFetch ( )
348
+ . get ( {
349
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
350
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
351
+ url : `https://api.netlify.com/api/v1/sites/${ siteID } /blobs/${ key } ?context=production` ,
352
+ } )
353
+ . get ( {
354
+ response : new Response ( 'Something went wrong' , { status : 404 } ) ,
355
+ url : signedURL ,
356
+ } )
357
+
358
+ globalThis . fetch = mockStore . fetch
359
+
360
+ const blobs = getStore ( {
361
+ name : 'production' ,
362
+ token : apiToken ,
363
+ siteID,
364
+ } )
365
+
366
+ expect ( await blobs . getWithMetadata ( key ) ) . toBeNull ( )
367
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
368
+ } )
369
+
370
+ test ( 'Throws when the metadata object cannot be parsed' , async ( ) => {
371
+ const responseHeaders = {
372
+ etag : '123456789' ,
373
+ 'x-amz-meta-user' : `b64;${ base64Encode ( `{"name": "Netlify", "cool` ) } ` ,
374
+ }
375
+ const mockStore = new MockFetch ( )
376
+ . get ( {
377
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
378
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
379
+ url : `https://api.netlify.com/api/v1/sites/${ siteID } /blobs/${ key } ?context=production` ,
380
+ } )
381
+ . get ( {
382
+ response : new Response ( value , { headers : responseHeaders } ) ,
383
+ url : signedURL ,
384
+ } )
385
+
386
+ globalThis . fetch = mockStore . fetch
387
+
388
+ const blobs = getStore ( {
389
+ name : 'production' ,
390
+ token : apiToken ,
391
+ siteID,
392
+ } )
393
+
394
+ await expect ( async ( ) => await blobs . getWithMetadata ( key ) ) . rejects . toThrowError (
395
+ 'An internal error occurred while trying to retrieve the metadata for an entry. Please try updating to the latest version of the Netlify Blobs client.' ,
396
+ )
397
+
398
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
399
+ } )
400
+ } )
401
+
402
+ describe ( 'With edge credentials' , ( ) => {
403
+ test ( 'Reads from the blob store and returns the etag and the metadata object' , async ( ) => {
404
+ const mockMetadata = {
405
+ name : 'Netlify' ,
406
+ cool : true ,
407
+ functions : [ 'edge' , 'serverless' ] ,
408
+ }
409
+ const responseHeaders = {
410
+ etag : '123456789' ,
411
+ 'x-amz-meta-user' : `b64;${ base64Encode ( mockMetadata ) } ` ,
412
+ }
413
+ const mockStore = new MockFetch ( )
414
+ . get ( {
415
+ headers : { authorization : `Bearer ${ edgeToken } ` } ,
416
+ response : new Response ( value , { headers : responseHeaders } ) ,
417
+ url : `${ edgeURL } /${ siteID } /production/${ key } ` ,
418
+ } )
419
+ . get ( {
420
+ headers : { authorization : `Bearer ${ edgeToken } ` } ,
421
+ response : new Response ( value , { headers : responseHeaders } ) ,
422
+ url : `${ edgeURL } /${ siteID } /production/${ key } ` ,
423
+ } )
424
+
425
+ globalThis . fetch = mockStore . fetch
426
+
427
+ const blobs = getStore ( {
428
+ edgeURL,
429
+ name : 'production' ,
430
+ token : edgeToken ,
431
+ siteID,
432
+ } )
433
+
434
+ const entry1 = await blobs . getWithMetadata ( key )
435
+ expect ( entry1 . data ) . toBe ( value )
436
+ expect ( entry1 . etag ) . toBe ( responseHeaders . etag )
437
+ expect ( entry1 . metadata ) . toEqual ( mockMetadata )
438
+
439
+ const entry2 = await blobs . getWithMetadata ( key , { type : 'stream' } )
440
+ expect ( await streamToString ( entry2 . data as unknown as NodeJS . ReadableStream ) ) . toBe ( value )
441
+ expect ( entry2 . etag ) . toBe ( responseHeaders . etag )
442
+ expect ( entry2 . metadata ) . toEqual ( mockMetadata )
443
+
444
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
445
+ } )
446
+ } )
447
+ } )
448
+
321
449
describe ( 'set' , ( ) => {
322
450
describe ( 'With API credentials' , ( ) => {
323
451
test ( 'Writes to the blob store' , async ( ) => {
@@ -361,19 +489,23 @@ describe('set', () => {
361
489
expect ( mockStore . fulfilled ) . toBeTruthy ( )
362
490
} )
363
491
364
- test ( 'Accepts an `expiration` parameter' , async ( ) => {
365
- const expiration = new Date ( Date . now ( ) + 15_000 )
492
+ test ( 'Accepts a `metadata` parameter' , async ( ) => {
493
+ const metadata = {
494
+ name : 'Netlify' ,
495
+ cool : true ,
496
+ functions : [ 'edge' , 'serverless' ] ,
497
+ }
498
+ const encodedMetadata = `b64;${ Buffer . from ( JSON . stringify ( metadata ) ) . toString ( 'base64' ) } `
366
499
const mockStore = new MockFetch ( )
367
500
. put ( {
368
501
headers : { authorization : `Bearer ${ apiToken } ` } ,
369
502
response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
370
- url : `https://api.netlify.com/api/v1/sites/${ siteID } /blobs/${ key } ?context=production` ,
503
+ url : `https://api.netlify.com/api/v1/sites/${ siteID } /blobs/${ key } ?context=production&metadata= ${ encodedMetadata } ` ,
371
504
} )
372
505
. put ( {
373
506
body : value ,
374
507
headers : {
375
508
'cache-control' : 'max-age=0, stale-while-revalidate=60' ,
376
- 'x-nf-expires-at' : expiration . getTime ( ) . toString ( ) ,
377
509
} ,
378
510
response : new Response ( null ) ,
379
511
url : signedURL ,
@@ -387,7 +519,7 @@ describe('set', () => {
387
519
siteID,
388
520
} )
389
521
390
- await blobs . set ( key , value , { expiration } )
522
+ await blobs . set ( key , value , { metadata } )
391
523
392
524
expect ( mockStore . fulfilled ) . toBeTruthy ( )
393
525
} )
@@ -620,33 +752,34 @@ describe('setJSON', () => {
620
752
expect ( mockStore . fulfilled ) . toBeTruthy ( )
621
753
} )
622
754
623
- test ( 'Accepts an `expiration ` parameter' , async ( ) => {
624
- const expiration = new Date ( Date . now ( ) + 15_000 )
625
- const mockStore = new MockFetch ( )
626
- . put ( {
627
- headers : { authorization : `Bearer ${ apiToken } ` } ,
628
- response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
629
- url : `https://api.netlify.com/api/v1/sites/ ${ siteID } /blobs/ ${ key } ?context=production` ,
630
- } )
631
- . put ( {
632
- body : JSON . stringify ( { value } ) ,
633
- headers : {
634
- 'cache-control' : 'max-age=0, stale-while-revalidate=60' ,
635
- 'x-nf-expires-at ' : expiration . getTime ( ) . toString ( ) ,
636
- } ,
637
- response : new Response ( null ) ,
638
- url : signedURL ,
639
- } )
755
+ test ( 'Accepts a `metadata ` parameter' , async ( ) => {
756
+ const metadata = {
757
+ name : 'Netlify' ,
758
+ cool : true ,
759
+ functions : [ 'edge' , 'serverless' ] ,
760
+ }
761
+ const encodedMetadata = `b64; ${ Buffer . from ( JSON . stringify ( metadata ) ) . toString ( 'base64' ) } `
762
+ const mockStore = new MockFetch ( ) . put ( {
763
+ body : JSON . stringify ( { value } ) ,
764
+ headers : {
765
+ authorization : `Bearer ${ edgeToken } ` ,
766
+ 'cache-control' : 'max-age=0, stale-while-revalidate=60' ,
767
+ 'netlify-blobs-metadata ' : encodedMetadata ,
768
+ } ,
769
+ response : new Response ( null ) ,
770
+ url : ` ${ edgeURL } / ${ siteID } /production/ ${ key } ` ,
771
+ } )
640
772
641
773
globalThis . fetch = mockStore . fetch
642
774
643
775
const blobs = getStore ( {
776
+ edgeURL,
644
777
name : 'production' ,
645
- token : apiToken ,
778
+ token : edgeToken ,
646
779
siteID,
647
780
} )
648
781
649
- await blobs . setJSON ( key , { value } , { expiration } )
782
+ await blobs . setJSON ( key , { value } , { metadata } )
650
783
651
784
expect ( mockStore . fulfilled ) . toBeTruthy ( )
652
785
} )
0 commit comments