@@ -132,7 +132,7 @@ describe('get', () => {
132
132
siteID,
133
133
} )
134
134
135
- expect ( async ( ) => await blobs . get ( key ) ) . rejects . toThrowError (
135
+ await expect ( async ( ) => await blobs . get ( key ) ) . rejects . toThrowError (
136
136
`Netlify Blobs has generated an internal error (401 status code, ID: ${ mockRequestID } )` ,
137
137
)
138
138
expect ( mockStore . fulfilled ) . toBeTruthy ( )
@@ -212,6 +212,126 @@ describe('get', () => {
212
212
213
213
expect ( mockStore . fulfilled ) . toBeTruthy ( )
214
214
} )
215
+
216
+ describe ( 'Conditional writes' , ( ) => {
217
+ test ( 'Returns `modified: false` when `onlyIfNew` is true and key exists' , async ( ) => {
218
+ const mockStore = new MockFetch ( )
219
+ . put ( {
220
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
221
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
222
+ url : `https://api.netlify.com/api/v1/blobs/${ siteID } /site:production/${ key } ` ,
223
+ } )
224
+ . put ( {
225
+ headers : { 'if-none-match' : '*' } ,
226
+ response : new Response ( null , { status : 412 } ) ,
227
+ url : signedURL ,
228
+ } )
229
+ . inject ( )
230
+
231
+ const blobs = getStore ( {
232
+ name : 'production' ,
233
+ token : apiToken ,
234
+ siteID,
235
+ } )
236
+
237
+ const result = await blobs . set ( key , value , {
238
+ onlyIfNew : true ,
239
+ } )
240
+
241
+ expect ( result . modified ) . toBe ( false )
242
+ expect ( result . etag ) . toBeUndefined ( )
243
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
244
+ } )
245
+
246
+ test ( 'Returns `modified: true` when `onlyIfNew` is true and key does not exist' , async ( ) => {
247
+ const mockStore = new MockFetch ( )
248
+ . put ( {
249
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
250
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
251
+ url : `https://api.netlify.com/api/v1/blobs/${ siteID } /site:production/${ key } ` ,
252
+ } )
253
+ . put ( {
254
+ headers : { 'if-none-match' : '*' } ,
255
+ response : new Response ( null , { status : 201 , headers : { etag : '"123"' } } ) ,
256
+ url : signedURL ,
257
+ } )
258
+ . inject ( )
259
+
260
+ const blobs = getStore ( {
261
+ name : 'production' ,
262
+ token : apiToken ,
263
+ siteID,
264
+ } )
265
+
266
+ const result = await blobs . set ( key , value , {
267
+ onlyIfNew : true ,
268
+ } )
269
+
270
+ expect ( result . modified ) . toBe ( true )
271
+ expect ( result . etag ) . toBe ( '"123"' )
272
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
273
+ } )
274
+
275
+ test ( 'Returns `modified: false` when `onlyIfMatch` does not match' , async ( ) => {
276
+ const etag = 'etag-123'
277
+ const mockStore = new MockFetch ( )
278
+ . put ( {
279
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
280
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
281
+ url : `https://api.netlify.com/api/v1/blobs/${ siteID } /site:production/${ key } ` ,
282
+ } )
283
+ . put ( {
284
+ headers : { 'if-match' : etag } ,
285
+ response : new Response ( null , { status : 412 } ) ,
286
+ url : signedURL ,
287
+ } )
288
+ . inject ( )
289
+
290
+ const blobs = getStore ( {
291
+ name : 'production' ,
292
+ token : apiToken ,
293
+ siteID,
294
+ } )
295
+
296
+ const result = await blobs . set ( key , value , {
297
+ onlyIfMatch : etag ,
298
+ } )
299
+
300
+ expect ( result . modified ) . toBe ( false )
301
+ expect ( result . etag ) . toBeUndefined ( )
302
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
303
+ } )
304
+
305
+ test ( 'Returns `modified: true` when `onlyIfMatch` matches' , async ( ) => {
306
+ const etag = 'etag-123'
307
+ const mockStore = new MockFetch ( )
308
+ . put ( {
309
+ headers : { authorization : `Bearer ${ apiToken } ` } ,
310
+ response : new Response ( JSON . stringify ( { url : signedURL } ) ) ,
311
+ url : `https://api.netlify.com/api/v1/blobs/${ siteID } /site:production/${ key } ` ,
312
+ } )
313
+ . put ( {
314
+ headers : { 'if-match' : etag } ,
315
+ response : new Response ( null , { status : 200 , headers : { etag : '"123"' } } ) ,
316
+ url : signedURL ,
317
+ } )
318
+ . inject ( )
319
+
320
+ const blobs = getStore ( {
321
+ name : 'production' ,
322
+ token : apiToken ,
323
+ siteID,
324
+ } )
325
+
326
+ const result = await blobs . set ( key , value , {
327
+ onlyIfMatch : etag ,
328
+ } )
329
+
330
+ expect ( result . modified ) . toBe ( true )
331
+ expect ( result . etag ) . toBe ( '"123"' )
332
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
333
+ } )
334
+ } )
215
335
} )
216
336
217
337
describe ( 'With edge credentials' , ( ) => {
@@ -289,6 +409,159 @@ describe('get', () => {
289
409
expect ( mockStore . fulfilled ) . toBeTruthy ( )
290
410
} )
291
411
412
+ describe ( 'Conditional writes' , ( ) => {
413
+ test ( 'Returns `modified: false` when `onlyIfNew` is true and key exists' , async ( ) => {
414
+ const mockStore = new MockFetch ( )
415
+ . put ( {
416
+ headers : { authorization : `Bearer ${ edgeToken } ` , 'if-none-match' : '*' } ,
417
+ response : new Response ( null , { status : 412 } ) ,
418
+ url : `${ edgeURL } /${ siteID } /site:production/${ key } ` ,
419
+ } )
420
+ . inject ( )
421
+
422
+ const blobs = getStore ( {
423
+ edgeURL,
424
+ name : 'production' ,
425
+ token : edgeToken ,
426
+ siteID,
427
+ } )
428
+
429
+ const result = await blobs . set ( key , value , {
430
+ onlyIfNew : true ,
431
+ } )
432
+
433
+ expect ( result . modified ) . toBe ( false )
434
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
435
+ } )
436
+
437
+ test ( 'Returns `modified: true` when `onlyIfNew` is true and key does not exist' , async ( ) => {
438
+ const mockStore = new MockFetch ( )
439
+ . put ( {
440
+ headers : { authorization : `Bearer ${ edgeToken } ` , 'if-none-match' : '*' } ,
441
+ response : new Response ( null , { status : 201 , headers : { etag : '"123"' } } ) ,
442
+ url : `${ edgeURL } /${ siteID } /site:production/${ key } ` ,
443
+ } )
444
+ . inject ( )
445
+
446
+ const blobs = getStore ( {
447
+ edgeURL,
448
+ name : 'production' ,
449
+ token : edgeToken ,
450
+ siteID,
451
+ } )
452
+
453
+ const result = await blobs . set ( key , value , {
454
+ onlyIfNew : true ,
455
+ } )
456
+
457
+ expect ( result . modified ) . toBe ( true )
458
+ expect ( result . etag ) . toBe ( '"123"' )
459
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
460
+ } )
461
+
462
+ test ( 'Returns `modified: false` when `onlyIfMatch` does not match' , async ( ) => {
463
+ const etag = 'etag-123'
464
+ const mockStore = new MockFetch ( )
465
+ . put ( {
466
+ headers : { authorization : `Bearer ${ edgeToken } ` , 'if-match' : etag } ,
467
+ response : new Response ( null , { status : 412 } ) ,
468
+ url : `${ edgeURL } /${ siteID } /site:production/${ key } ` ,
469
+ } )
470
+ . inject ( )
471
+
472
+ const blobs = getStore ( {
473
+ edgeURL,
474
+ name : 'production' ,
475
+ token : edgeToken ,
476
+ siteID,
477
+ } )
478
+
479
+ const result = await blobs . set ( key , value , {
480
+ onlyIfMatch : etag ,
481
+ } )
482
+
483
+ expect ( result . modified ) . toBe ( false )
484
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
485
+ } )
486
+
487
+ test ( 'Returns `modified: true` when `onlyIfMatch` matches' , async ( ) => {
488
+ const etag = 'etag-123'
489
+ const mockStore = new MockFetch ( )
490
+ . put ( {
491
+ headers : { authorization : `Bearer ${ edgeToken } ` , 'if-match' : etag } ,
492
+ response : new Response ( null , { status : 200 , headers : { etag : '"123"' } } ) ,
493
+ url : `${ edgeURL } /${ siteID } /site:production/${ key } ` ,
494
+ } )
495
+ . inject ( )
496
+
497
+ const blobs = getStore ( {
498
+ edgeURL,
499
+ name : 'production' ,
500
+ token : edgeToken ,
501
+ siteID,
502
+ } )
503
+
504
+ const result = await blobs . set ( key , value , {
505
+ onlyIfMatch : etag ,
506
+ } )
507
+
508
+ expect ( result . modified ) . toBe ( true )
509
+ expect ( result . etag ) . toBe ( '"123"' )
510
+ expect ( mockStore . fulfilled ) . toBeTruthy ( )
511
+ } )
512
+
513
+ test ( 'Throws an error when both `onlyIfNew` and `onlyIfMatch` are provided' , async ( ) => {
514
+ const blobs = getStore ( {
515
+ name : 'production' ,
516
+ token : apiToken ,
517
+ siteID,
518
+ } )
519
+
520
+ await expect (
521
+ blobs . set ( key , value , {
522
+ onlyIfNew : true ,
523
+
524
+ // @ts -expect-error Testing runtime validation
525
+ onlyIfMatch : '"123"' ,
526
+ } ) ,
527
+ ) . rejects . toThrow (
528
+ `The 'onlyIfMatch' and 'onlyIfNew' options are mutually exclusive. Using 'onlyIfMatch' will make the write succeed only if there is an entry for the key with the given content, while 'onlyIfNew' will make the write succeed only if there is no entry for the key.` ,
529
+ )
530
+ } )
531
+
532
+ test ( 'Throws an error when `onlyIfMatch` is not a string' , async ( ) => {
533
+ const blobs = getStore ( {
534
+ name : 'production' ,
535
+ token : apiToken ,
536
+ siteID,
537
+ } )
538
+
539
+ await expect (
540
+ blobs . set ( key , value , {
541
+ // @ts -expect-error Testing runtime validation
542
+ onlyIfMatch : 123 ,
543
+ } ) ,
544
+ ) . rejects . toThrow ( `The 'onlyIfMatch' property expects a string representing an ETag.` )
545
+ } )
546
+
547
+ test ( 'Throws an error when `onlyIfNew` is not a boolean' , async ( ) => {
548
+ const blobs = getStore ( {
549
+ name : 'production' ,
550
+ token : apiToken ,
551
+ siteID,
552
+ } )
553
+
554
+ await expect (
555
+ blobs . set ( key , value , {
556
+ // @ts -expect-error Testing runtime validation
557
+ onlyIfNew : 'yes' ,
558
+ } ) ,
559
+ ) . rejects . toThrow (
560
+ `The 'onlyIfNew' property expects a boolean indicating whether the write should fail if an entry for the key already exists.` ,
561
+ )
562
+ } )
563
+ } )
564
+
292
565
describe ( 'Loads credentials from the environment' , ( ) => {
293
566
test ( 'From the `NETLIFY_BLOBS_CONTEXT` environment variable' , async ( ) => {
294
567
const tokens = [ 'some-token-1' , 'another-token-2' ]
@@ -804,7 +1077,7 @@ describe('set', () => {
804
1077
siteID,
805
1078
} )
806
1079
807
- expect ( async ( ) => await blobs . set ( key , 'value' ) ) . rejects . toThrowError (
1080
+ await expect ( async ( ) => await blobs . set ( key , 'value' ) ) . rejects . toThrowError (
808
1081
`Netlify Blobs has generated an internal error (401 status code)` ,
809
1082
)
810
1083
expect ( mockStore . fulfilled ) . toBeTruthy ( )
@@ -819,11 +1092,11 @@ describe('set', () => {
819
1092
siteID,
820
1093
} )
821
1094
822
- expect ( async ( ) => await blobs . set ( '' , 'value' ) ) . rejects . toThrowError ( 'Blob key must not be empty.' )
823
- expect ( async ( ) => await blobs . set ( '/key' , 'value' ) ) . rejects . toThrowError (
1095
+ await expect ( async ( ) => await blobs . set ( '' , 'value' ) ) . rejects . toThrowError ( 'Blob key must not be empty.' )
1096
+ await expect ( async ( ) => await blobs . set ( '/key' , 'value' ) ) . rejects . toThrowError (
824
1097
'Blob key must not start with forward slash (/).' ,
825
1098
)
826
- expect ( async ( ) => await blobs . set ( 'a' . repeat ( 801 ) , 'value' ) ) . rejects . toThrowError (
1099
+ await expect ( async ( ) => await blobs . set ( 'a' . repeat ( 801 ) , 'value' ) ) . rejects . toThrowError (
827
1100
'Blob key must be a sequence of Unicode characters whose UTF-8 encoding is at most 600 bytes long.' ,
828
1101
)
829
1102
} )
@@ -1076,7 +1349,7 @@ describe('setJSON', () => {
1076
1349
siteID,
1077
1350
} )
1078
1351
1079
- expect ( async ( ) => await blobs . setJSON ( key , { value } , { metadata } ) ) . rejects . toThrowError (
1352
+ await expect ( async ( ) => await blobs . setJSON ( key , { value } , { metadata } ) ) . rejects . toThrowError (
1080
1353
'Metadata object exceeds the maximum size' ,
1081
1354
)
1082
1355
} )
0 commit comments