@toa.io/extensions.storages 1.0.0-alpha.21 → 1.0.0-alpha.219

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.
Files changed (91) hide show
  1. package/package.json +14 -14
  2. package/readme.md +96 -51
  3. package/schemas/annotation.cos.yaml +1 -0
  4. package/schemas/cloudinary.cos.yaml +37 -0
  5. package/schemas/fs.cos.yaml +2 -0
  6. package/schemas/s3.cos.yaml +1 -0
  7. package/schemas/tmp.cos.yaml +0 -1
  8. package/source/Entry.ts +13 -11
  9. package/source/Factory.ts +16 -9
  10. package/source/Provider.ts +22 -19
  11. package/source/Scanner.ts +58 -26
  12. package/source/Secrets.ts +6 -0
  13. package/source/Storage.test.ts +118 -274
  14. package/source/Storage.ts +62 -207
  15. package/source/deployment.ts +52 -16
  16. package/source/errors.ts +3 -0
  17. package/source/index.ts +3 -1
  18. package/source/manifest.ts +7 -6
  19. package/source/providers/Cloudinary.ts +320 -0
  20. package/source/providers/Declaration.ts +2 -1
  21. package/source/providers/FileSystem.ts +79 -25
  22. package/source/providers/S3.ts +81 -80
  23. package/source/providers/Temporary.ts +2 -3
  24. package/source/providers/Test.ts +4 -4
  25. package/source/providers/index.test.ts +102 -76
  26. package/source/providers/index.ts +10 -4
  27. package/source/schemas.ts +1 -0
  28. package/source/test/.env.example +4 -0
  29. package/source/test/arny.jpg +0 -0
  30. package/source/test/lenna.48x48.jpeg +0 -0
  31. package/source/test/plank.mp4 +0 -0
  32. package/source/test/sample.avi +0 -0
  33. package/source/test/sample.wav +0 -0
  34. package/source/test/sport.mp4 +0 -0
  35. package/source/test/util.ts +62 -12
  36. package/transpiled/Entry.d.ts +15 -11
  37. package/transpiled/Factory.js +11 -5
  38. package/transpiled/Factory.js.map +1 -1
  39. package/transpiled/Provider.d.ts +17 -16
  40. package/transpiled/Provider.js +6 -4
  41. package/transpiled/Provider.js.map +1 -1
  42. package/transpiled/Scanner.d.ts +6 -2
  43. package/transpiled/Scanner.js +49 -22
  44. package/transpiled/Scanner.js.map +1 -1
  45. package/transpiled/Secrets.d.ts +5 -0
  46. package/transpiled/Secrets.js +3 -0
  47. package/transpiled/Secrets.js.map +1 -0
  48. package/transpiled/Storage.d.ts +13 -21
  49. package/transpiled/Storage.js +52 -155
  50. package/transpiled/Storage.js.map +1 -1
  51. package/transpiled/deployment.d.ts +1 -1
  52. package/transpiled/deployment.js +43 -16
  53. package/transpiled/deployment.js.map +1 -1
  54. package/transpiled/errors.d.ts +2 -0
  55. package/transpiled/errors.js +6 -0
  56. package/transpiled/errors.js.map +1 -0
  57. package/transpiled/index.d.ts +3 -1
  58. package/transpiled/manifest.js +5 -2
  59. package/transpiled/manifest.js.map +1 -1
  60. package/transpiled/providers/Cloudinary.d.ts +51 -0
  61. package/transpiled/providers/Cloudinary.js +223 -0
  62. package/transpiled/providers/Cloudinary.js.map +1 -0
  63. package/transpiled/providers/Declaration.d.ts +3 -2
  64. package/transpiled/providers/FileSystem.d.ts +15 -7
  65. package/transpiled/providers/FileSystem.js +64 -24
  66. package/transpiled/providers/FileSystem.js.map +1 -1
  67. package/transpiled/providers/S3.d.ts +14 -14
  68. package/transpiled/providers/S3.js +62 -60
  69. package/transpiled/providers/S3.js.map +1 -1
  70. package/transpiled/providers/Temporary.d.ts +1 -2
  71. package/transpiled/providers/Temporary.js +2 -2
  72. package/transpiled/providers/Temporary.js.map +1 -1
  73. package/transpiled/providers/Test.d.ts +3 -3
  74. package/transpiled/providers/Test.js +2 -2
  75. package/transpiled/providers/Test.js.map +1 -1
  76. package/transpiled/providers/index.d.ts +7 -2
  77. package/transpiled/providers/index.js +2 -2
  78. package/transpiled/providers/index.js.map +1 -1
  79. package/transpiled/schemas.d.ts +1 -0
  80. package/transpiled/schemas.js +2 -1
  81. package/transpiled/schemas.js.map +1 -1
  82. package/transpiled/test/util.d.ts +89 -5
  83. package/transpiled/test/util.js +51 -4
  84. package/transpiled/test/util.js.map +1 -1
  85. package/transpiled/tsconfig.tsbuildinfo +1 -1
  86. package/source/providers/FileSystem.test.ts +0 -5
  87. package/source/providers/Memory.ts +0 -41
  88. package/source/providers/S3.test.ts +0 -133
  89. package/transpiled/providers/Memory.d.ts +0 -13
  90. package/transpiled/providers/Memory.js +0 -60
  91. package/transpiled/providers/Memory.js.map +0 -1
@@ -1,4 +1,3 @@
1
- import { Readable } from 'node:stream'
2
1
  import { randomUUID } from 'node:crypto'
3
2
  import { buffer } from 'node:stream/consumers'
4
3
  import { createReadStream } from 'node:fs'
@@ -6,28 +5,22 @@ import path from 'node:path'
6
5
  import assert from 'node:assert'
7
6
  import { Storage } from './Storage'
8
7
  import { suites } from './test/util'
9
- import { type Entry } from './Entry'
10
8
  import { providers } from './providers'
11
- import type { ProviderConstructor } from './Provider'
9
+ import type { Entry, Metadata, Stream } from './Entry'
10
+ import type { Constructor } from './Provider'
12
11
 
13
- let storage: Storage
14
- let dir: string
12
+ jest.setTimeout(15_000)
15
13
 
16
14
  const suite = suites[0]
15
+ const Provider: Constructor = providers[suite.provider]
16
+ const provider = new Provider(suite.options, suite.secrets)
17
+ const storage = new Storage(provider)
18
+ const dir = '/' + randomUUID()
17
19
 
18
20
  beforeAll(async () => {
19
21
  process.chdir(path.join(__dirname, 'test'))
20
22
  })
21
23
 
22
- beforeEach(() => {
23
- dir = '/' + randomUUID()
24
-
25
- const Provider: ProviderConstructor = providers[suite.provider]
26
- const provider = new Provider(suite.options)
27
-
28
- storage = new Storage(provider)
29
- })
30
-
31
24
  it('should be', async () => {
32
25
  expect(storage).toBeInstanceOf(Storage)
33
26
  })
@@ -40,6 +33,7 @@ it('should return error if entry is not found', async () => {
40
33
  })
41
34
 
42
35
  describe('put', () => {
36
+ let path: string
43
37
  let lenna: Entry
44
38
  let startCreation: number
45
39
 
@@ -47,7 +41,10 @@ describe('put', () => {
47
41
  const stream = createReadStream('lenna.png')
48
42
 
49
43
  startCreation = Date.now()
50
- lenna = (await storage.put(dir, stream)) as Entry
44
+ lenna = await storage.put(dir, stream) as Entry
45
+ path = `${dir}/${lenna.id}`
46
+
47
+ assert(!(lenna instanceof Error))
51
48
  })
52
49
 
53
50
  it('should not return error', async () => {
@@ -56,14 +53,7 @@ describe('put', () => {
56
53
 
57
54
  it('should return entry id', async () => {
58
55
  expect(lenna.id).toBeDefined()
59
- })
60
-
61
- it('should return id as checksum', async () => {
62
- const stream = createReadStream('lenna.png')
63
- const dir2 = '/' + randomUUID()
64
- const copy = (await storage.put(dir2, stream)) as Entry
65
-
66
- expect(copy).toHaveProperty('id', lenna.id)
56
+ expect(lenna.id).toHaveLength(32)
67
57
  })
68
58
 
69
59
  it('should detect file type', async () => {
@@ -78,257 +68,96 @@ describe('put', () => {
78
68
  expect(lenna).toMatchObject({
79
69
  id: lenna.id,
80
70
  type: 'image/png',
81
- variants: [],
82
- meta: {}
83
- })
71
+ size: 473831,
72
+ checksum: lenna.checksum,
73
+ created: expect.any(String),
74
+ attributes: {}
75
+ } satisfies Entry)
84
76
  })
85
77
 
86
- it('should create entry', async () => {
87
- const entry = await storage.get(`${dir}/${lenna.id}`)
78
+ it('should create metadata', async () => {
79
+ const entry = await storage.head(path) as Entry
88
80
 
89
81
  expect(entry).toMatchObject({
90
- id: lenna.id,
91
82
  type: 'image/png',
92
- variants: [],
93
- meta: {}
94
- })
83
+ size: 473831,
84
+ checksum: lenna.checksum,
85
+ created: expect.any(String),
86
+ attributes: {}
87
+ } satisfies Metadata)
95
88
  })
96
89
 
97
90
  it('should set timestamp', async () => {
98
- const now = Date.now()
99
- const entry = (await storage.get(`${dir}/${lenna.id}`)) as Entry
100
-
101
- expect(entry.created).toBeLessThanOrEqual(now)
102
- expect(entry.created).toBeGreaterThanOrEqual(startCreation)
103
- })
104
-
105
- describe('existing entry', () => {
106
- it('should unhide existing', async () => {
107
- const stream = createReadStream('lenna.png')
108
- const path = `${dir}/${lenna.id}`
109
-
110
- await storage.conceal(path)
111
- await storage.put(dir, stream)
112
-
113
- const list = await storage.list(dir)
114
-
115
- expect(list).toContainEqual(lenna.id)
116
- })
117
-
118
- it('should preserve meta', async () => {
119
- const path = `${dir}/${lenna.id}`
120
- const stream = createReadStream('lenna.png')
121
-
122
- await storage.annotate(path, 'foo', 'bar')
123
- await storage.put(dir, stream)
124
-
125
- const entry = await storage.get(path)
126
-
127
- expect(entry).toHaveProperty('meta', expect.objectContaining({ foo: 'bar' }))
128
- })
129
- })
130
- })
131
-
132
- describe('list', () => {
133
- let albert: Entry
134
- let lenna: Entry
135
-
136
- beforeEach(async () => {
137
- const stream0 = createReadStream('albert.jpg')
138
- const stream1 = createReadStream('lenna.png')
139
-
140
- albert = (await storage.put(dir, stream0)) as Entry
141
- lenna = (await storage.put(dir, stream1)) as Entry
142
- })
143
-
144
- it('should list entries', async () => {
145
- const list = await storage.list(dir)
146
-
147
- expect(list).toEqual([albert.id, lenna.id])
148
- })
91
+ const now = Date.now() + 1000 // Cloudinary has 1s resolution
92
+ const entry = await storage.get(path) as Stream
93
+ const created = new Date(entry.created).getTime()
149
94
 
150
- it('should permutate', async () => {
151
- const error = await storage.permute(dir, [lenna.id, albert.id])
152
-
153
- expect(error).toBeUndefined()
154
-
155
- const list = await storage.list(dir)
156
-
157
- expect(list).toEqual([lenna.id, albert.id])
158
- })
159
-
160
- it('should return PERMUTATION_MISMATCH', async () => {
161
- const cases = [
162
- [lenna.id],
163
- [albert.id, lenna.id, 'unknown'],
164
- [lenna.id, lenna.id],
165
- [lenna.id, lenna.id, albert.id]
166
- ]
167
-
168
- for (const permutation of cases) {
169
- const error = await storage.permute(dir, permutation)
170
-
171
- expect(error).toBeInstanceOf(Error)
172
- expect(error).toHaveProperty('code', 'PERMUTATION_MISMATCH')
173
- }
174
- })
175
-
176
- it('should exclude concealed', async () => {
177
- const path = `${dir}/${lenna.id}`
178
-
179
- await storage.conceal(path)
180
-
181
- const entries = await storage.list(dir)
182
-
183
- expect(entries).toEqual([albert.id])
95
+ expect(created).toBeLessThanOrEqual(now)
96
+ expect(created).toBeGreaterThanOrEqual(startCreation)
184
97
  })
185
98
 
186
- it('should reveal', async () => {
187
- const path = `${dir}/${lenna.id}`
188
-
189
- await storage.conceal(path)
190
- await storage.reveal(path)
191
- await storage.reveal(path) // test that no duplicates are created
192
-
193
- const entries = await storage.list(dir)
194
-
195
- expect(entries).toEqual([albert.id, lenna.id])
196
- })
197
-
198
- it('should return ERR_NOT_FOOUD if entry doesnt exist', async () => {
199
- const path = `${dir}/oopsie`
200
-
201
- const methods: Array<'reveal' | 'conceal'> = ['reveal', 'conceal']
202
-
203
- for (const method of methods) {
204
- const error = await storage[method](path)
205
-
206
- expect(error).toBeInstanceOf(Error)
207
- expect(error).toHaveProperty('code', 'NOT_FOUND')
208
- }
209
- })
210
- })
211
-
212
- describe('annotate', () => {
213
- let lenna: Entry
214
-
215
- beforeEach(async () => {
99
+ it('should store with given id', async () => {
216
100
  const stream = createReadStream('lenna.png')
101
+ const id = Math.random().toString(36).slice(2)
217
102
 
218
- lenna = (await storage.put(dir, stream)) as Entry
219
- })
103
+ const entry = await storage.put(dir, stream, { id }) as Entry
220
104
 
221
- it('should set meta', async () => {
222
- const path = `${dir}/${lenna.id}`
105
+ expect(entry.id).toBe(id)
223
106
 
224
- await storage.annotate(path, 'foo', 'bar')
107
+ const check = await storage.head(`${dir}/${id}`)
225
108
 
226
- const state0 = (await storage.get(path)) as Entry
109
+ if (check instanceof Error)
110
+ throw check
227
111
 
228
- expect(state0).toHaveProperty('meta.foo', 'bar')
229
-
230
- await storage.annotate(path, 'foo')
231
-
232
- const state1 = (await storage.get(path)) as Entry
233
-
234
- expect(state1.meta).not.toHaveProperty('foo')
112
+ expect(check.id).toBe(id)
235
113
  })
236
114
  })
237
115
 
238
- describe('variants', () => {
116
+ describe('get, head', () => {
239
117
  let lenna: Entry
118
+ let path: string
240
119
 
241
120
  beforeEach(async () => {
242
121
  const stream = createReadStream('lenna.png')
243
122
 
244
- lenna = (await storage.put(dir, stream)) as Entry
245
- })
246
-
247
- it('should add variant', async () => {
248
- const stream = createReadStream('sample.jpeg')
249
-
250
- const path = `${dir}/${lenna.id}`
251
-
252
- await storage.diversify(path, 'foo', stream)
123
+ lenna = await storage.put(dir, stream) as Entry
253
124
 
254
- const state = (await storage.get(path)) as Entry
255
-
256
- expect(state).toHaveProperty('variants',
257
- expect.arrayContaining([{ name: 'foo', size: 73444, type: 'image/jpeg' }]))
258
- })
259
-
260
- it('should replace variant', async () => {
261
- const stream0 = createReadStream('sample.jpeg')
262
- const stream1 = createReadStream('sample.webp')
263
- const path = `${dir}/${lenna.id}`
264
-
265
- await storage.diversify(path, 'foo', stream0)
266
- await storage.diversify(path, 'foo', stream1)
267
-
268
- const state = (await storage.get(path)) as Entry
269
-
270
- expect(state).toHaveProperty('variants',
271
- expect.arrayContaining([expect.objectContaining({ name: 'foo', type: 'image/webp' })]))
272
- })
273
- })
274
-
275
- describe('fetch', () => {
276
- let lenna: Entry
277
-
278
- beforeEach(async () => {
279
- const stream = createReadStream('lenna.png')
280
-
281
- lenna = (await storage.put(dir, stream)) as Entry
125
+ path = `${dir}/${lenna.id}`
282
126
  })
283
127
 
284
- it('should fetch', async () => {
285
- const path = `${dir}/${lenna.id}`
286
- const stream = await storage.fetch(path)
287
-
288
- assert.ok(stream instanceof Readable)
289
-
290
- const stored = await buffer(stream)
128
+ it('should get', async () => {
129
+ const entry = await storage.get(path) as Stream
130
+ const stored = await buffer(entry.stream)
291
131
  const buf = await buffer(createReadStream('lenna.png'))
292
132
 
293
- expect(stored.compare(buf)).toBe(0)
133
+ expect(stored.compare(new Uint8Array(buf.buffer))).toBe(0)
294
134
  })
295
135
 
296
- it('should fetch blob by id', async () => {
297
- const stream = createReadStream('lenna.ascii')
298
- const entry = (await storage.put(dir, stream)) as Entry
299
- const stored = await storage.fetch(entry.id)
300
-
301
- if (stored instanceof Error) throw stored
136
+ it('should get entry', async () => {
137
+ const entry = await storage.head(path) as Entry
302
138
 
303
- const buf = await buffer(stored)
304
- const expected = await buffer(createReadStream('lenna.ascii'))
305
-
306
- expect(buf.compare(expected)).toBe(0)
139
+ expect(entry.id).toBe(lenna.id)
307
140
  })
308
141
 
309
- it('should fetch variant', async () => {
310
- const stream = createReadStream('sample.jpeg')
311
-
312
- const buf = await buffer(stream)
313
- const path = `${dir}/${lenna.id}`
314
-
315
- await storage.diversify(path, '100x100.jpeg', Readable.from(buf))
316
-
317
- const variant = await storage.fetch(`${path}.100x100.jpeg`)
142
+ if (suite.provider === 'cloudinary') {
143
+ it('should return cloudinary url', async () => {
144
+ const entry = await storage.head(`${path}.jpeg`) as Entry
318
145
 
319
- assert.ok(variant instanceof Readable)
146
+ expect(entry.attributes.url).toMatch(/^https:\/\//)
147
+ })
320
148
 
321
- const stored = await buffer(variant)
149
+ it('should support conditions', async () => {
150
+ const entry = await storage.head(`${path}.vertical.jpeg`) as Entry
322
151
 
323
- expect(stored.compare(buf)).toBe(0)
324
- })
152
+ expect(entry.attributes.url.includes('/if_w_gt_h/a_90/if_end/')).toBe(true)
153
+ })
325
154
 
326
- it('should not fetch blob by id and fake path', async () => {
327
- const stored = await storage.fetch(`fake/${lenna.id}`)
155
+ it('should return entry id when requested with extensions', async () => {
156
+ const entry = await storage.head(`${path}.jpeg`) as Entry
328
157
 
329
- expect(stored).toBeInstanceOf(Error)
330
- expect(stored).toHaveProperty('code', 'NOT_FOUND')
331
- })
158
+ expect(entry.id).toBe(lenna.id)
159
+ })
160
+ }
332
161
  })
333
162
 
334
163
  describe('delete', () => {
@@ -337,46 +166,26 @@ describe('delete', () => {
337
166
  beforeEach(async () => {
338
167
  const stream = createReadStream('lenna.png')
339
168
 
340
- lenna = (await storage.put(dir, stream)) as Entry
341
- })
342
-
343
- it('should remove from the list', async () => {
344
- await storage.delete(`${dir}/${lenna.id}`)
345
-
346
- const list = await storage.list(dir)
347
-
348
- expect(list).not.toContain(lenna.id)
169
+ lenna = await storage.put(dir, stream) as Entry
349
170
  })
350
171
 
351
172
  it('should delete entry', async () => {
352
173
  await storage.delete(`${dir}/${lenna.id}`)
353
174
 
175
+ // Cloudinary needs some time to invalidate the cache
176
+ if (suite.provider === 'cloudinary')
177
+ await new Promise((resolve) => setTimeout(resolve, 10_000))
178
+
354
179
  const result = await storage.get(`${dir}/${lenna.id}`)
355
180
 
356
181
  expect(result).toBeInstanceOf(Error)
357
182
  expect(result).toHaveProperty('code', 'NOT_FOUND')
358
183
  })
359
184
 
360
- it('should delete variants', async () => {
361
- const stream = createReadStream('sample.jpeg')
362
-
363
- const path = `${dir}/${lenna.id}`
364
-
365
- await storage.diversify(path, 'foo', stream)
366
- await storage.delete(`${dir}/${lenna.id}`)
367
- stream.destroy()
368
-
369
- const variant = await storage.fetch(`${path}.foo`)
370
-
371
- expect(variant).toBeInstanceOf(Error)
372
- expect(variant).toHaveProperty('code', 'NOT_FOUND')
373
- })
374
-
375
- it('should throw if path is not an entry', async () => {
185
+ it('should not return error', async () => {
376
186
  const result = await storage.delete(dir)
377
187
 
378
- expect(result).toBeInstanceOf(Error)
379
- expect(result).toMatchObject({ code: 'NOT_FOUND' })
188
+ expect(result).toBeUndefined()
380
189
  })
381
190
  })
382
191
 
@@ -384,14 +193,36 @@ describe('signatures', () => {
384
193
  it.each(['jpeg', 'gif', 'webp', 'heic', 'jxl', 'avif'])('should detect image/%s',
385
194
  async (type) => {
386
195
  const stream = createReadStream('sample.' + type)
196
+ const entry = await storage.put(dir, stream) as Entry
197
+
198
+ expect(entry).toHaveProperty('type', 'image/' + type)
199
+ })
200
+
201
+ it.each(['avi'])('should detect video/%s',
202
+ async (type) => {
203
+ const stream = createReadStream('sample.' + type)
204
+ const entry = await storage.put(dir, stream) as Entry
205
+
206
+ expect(entry).toHaveProperty('type', 'video/' + type)
207
+ })
387
208
 
209
+ it.each(['wav'])('should detect audio/%s',
210
+ async (type) => {
211
+ const stream = createReadStream('sample.' + type)
388
212
  const entry = (await storage.put(dir, stream)) as Entry
389
213
 
390
- expect(entry).toHaveProperty('type', 'image/' + type)
214
+ expect(entry).toHaveProperty('type', 'audio/' + type)
391
215
  })
216
+
217
+ it('should be ok with Arny', async () => {
218
+ const stream = createReadStream('arny.jpg')
219
+ const entry = (await storage.put(dir, stream)) as Entry
220
+
221
+ expect(entry).toHaveProperty('type', 'image/jpeg')
222
+ })
392
223
  })
393
224
 
394
- it("should return error if type doesn't match", async () => {
225
+ it('should return error if type doesn\'t match', async () => {
395
226
  const stream = createReadStream('sample.jpeg')
396
227
 
397
228
  const result = await storage.put(dir, stream, { claim: 'image/png' })
@@ -447,27 +278,40 @@ it('should accept wildcard types', async () => {
447
278
 
448
279
  it('should handle root entries', async () => {
449
280
  const stream = createReadStream('sample.jpeg')
450
-
451
- const result = (await storage.put('hello', stream)) as Entry
281
+ const result = await storage.put('/', stream) as Entry
452
282
 
453
283
  expect(result).not.toBeInstanceOf(Error)
454
284
 
455
- const stored = await storage.fetch(result.id)
285
+ const stored = await storage.get(result.id)
456
286
 
457
287
  expect(stored).not.toBeInstanceOf(Error)
458
288
  })
459
289
 
460
- it('should store empty file', async () => {
461
- const stream = createReadStream('empty.txt')
462
- const result = (await storage.put('empty', stream)) as Entry
290
+ it('should return error of stream size limit exceeded', async () => {
291
+ const stream = createReadStream('sample.jpeg')
292
+ const result = await storage.put(dir, stream, { limit: 1024 })
463
293
 
464
- expect(result).toHaveProperty('size', 0)
294
+ expect(result).toBeInstanceOf(Error)
295
+ expect(result).toHaveProperty('code', 'LIMIT_EXCEEDED')
296
+ })
465
297
 
466
- const stored = (await storage.fetch(result.id)) as Readable
298
+ if (suite.provider !== 'cloudinary')
299
+ it('should add origin attribute', async () => {
300
+ const origin = 'https://example.com/image.jpeg'
301
+ const stream = createReadStream('sample.jpeg')
302
+ const result = await storage.put('/origins', stream, { origin }) as Entry
467
303
 
468
- expect(stored).not.toBeInstanceOf(Error)
304
+ assert.ok(!(result instanceof Error))
305
+
306
+ const entry = await storage.get('/origins/' + result.id)
307
+
308
+ assert.ok(!(entry instanceof Error))
309
+
310
+ expect(entry.attributes.origin).toBe(origin)
311
+ })
469
312
 
470
- const buf = await buffer(stored)
313
+ it('should expose path', () => {
314
+ const path = storage.path()
471
315
 
472
- expect(buf).toHaveLength(0)
316
+ expect(typeof path === 'string' || path === null)
473
317
  })