@interop/was-conformance-suite 0.15.0 → 0.16.0
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.
- package/README.md +1 -0
- package/dist/suites/authz-ordering-api.d.ts.map +1 -1
- package/dist/suites/authz-ordering-api.js +1 -26
- package/dist/suites/authz-ordering-api.js.map +1 -1
- package/dist/suites/blinded-index-api.js +1 -1
- package/dist/suites/blinded-index-api.js.map +1 -1
- package/dist/suites/changes-query-api.js +1 -1
- package/dist/suites/changes-query-api.js.map +1 -1
- package/dist/suites/chunks-api.js +1 -1
- package/dist/suites/chunks-api.js.map +1 -1
- package/dist/suites/client-spaces.d.ts.map +1 -1
- package/dist/suites/client-spaces.js +19 -6
- package/dist/suites/client-spaces.js.map +1 -1
- package/dist/suites/collection-api.d.ts.map +1 -1
- package/dist/suites/collection-api.js +317 -126
- package/dist/suites/collection-api.js.map +1 -1
- package/dist/suites/conditional-requests-api.d.ts.map +1 -1
- package/dist/suites/conditional-requests-api.js +122 -30
- package/dist/suites/conditional-requests-api.js.map +1 -1
- package/dist/suites/delegation-cryptosuites-api.js +1 -1
- package/dist/suites/delegation-cryptosuites-api.js.map +1 -1
- package/dist/suites/denial-reasons-api.d.ts.map +1 -1
- package/dist/suites/denial-reasons-api.js +5 -2
- package/dist/suites/denial-reasons-api.js.map +1 -1
- package/dist/suites/digest-api.js +1 -1
- package/dist/suites/digest-api.js.map +1 -1
- package/dist/suites/encryption-descriptor-api.d.ts.map +1 -1
- package/dist/suites/encryption-descriptor-api.js +48 -51
- package/dist/suites/encryption-descriptor-api.js.map +1 -1
- package/dist/suites/governed-log-api.d.ts.map +1 -1
- package/dist/suites/governed-log-api.js +18 -17
- package/dist/suites/governed-log-api.js.map +1 -1
- package/dist/suites/index.d.ts +2 -1
- package/dist/suites/index.d.ts.map +1 -1
- package/dist/suites/index.js +3 -1
- package/dist/suites/index.js.map +1 -1
- package/dist/suites/invocation-target-api.js +1 -1
- package/dist/suites/invocation-target-api.js.map +1 -1
- package/dist/suites/plaintext-declaration-api.d.ts.map +1 -1
- package/dist/suites/plaintext-declaration-api.js +24 -15
- package/dist/suites/plaintext-declaration-api.js.map +1 -1
- package/dist/suites/policy-api.js +1 -1
- package/dist/suites/policy-api.js.map +1 -1
- package/dist/suites/reserved-methods-api.d.ts +9 -0
- package/dist/suites/reserved-methods-api.d.ts.map +1 -0
- package/dist/suites/reserved-methods-api.js +549 -0
- package/dist/suites/reserved-methods-api.js.map +1 -0
- package/dist/suites/resource-api.js +1 -1
- package/dist/suites/resource-api.js.map +1 -1
- package/dist/suites/spaces-api.d.ts.map +1 -1
- package/dist/suites/spaces-api.js +193 -63
- package/dist/suites/spaces-api.js.map +1 -1
- package/dist/suites/write-validation-api.d.ts.map +1 -1
- package/dist/suites/write-validation-api.js +16 -36
- package/dist/suites/write-validation-api.js.map +1 -1
- package/package.json +3 -3
|
@@ -33,27 +33,6 @@ async function getWithCapability({ url, capability, invocationSigner }) {
|
|
|
33
33
|
headers: signatureHeaders
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Provisions a fresh Collection with a random id, returning its `/meta` URL and
|
|
38
|
-
* the Collection URL it hangs off. Every Collection Metadata test works on its
|
|
39
|
-
* own Collection, so the `metaVersion` sequence a test observes is its own.
|
|
40
|
-
*
|
|
41
|
-
* @param options {object}
|
|
42
|
-
* @param options.ctx {any} the test context (for `serverUrl` / `generateId`)
|
|
43
|
-
* @param options.alice {any} the suite's root actor
|
|
44
|
-
* @returns {Promise<{collectionUrl: string, metaUrl: string}>}
|
|
45
|
-
*/
|
|
46
|
-
async function freshCollection({ ctx, alice }) {
|
|
47
|
-
const collectionId = ctx.generateId();
|
|
48
|
-
const collectionUrl = new URL(`/space/${alice.space1.id}/${collectionId}`, ctx.serverUrl).toString();
|
|
49
|
-
await alice.rootClient.request({
|
|
50
|
-
url: collectionUrl,
|
|
51
|
-
method: 'PUT',
|
|
52
|
-
action: 'PUT',
|
|
53
|
-
json: { id: collectionId, name: 'Metadata Collection' }
|
|
54
|
-
});
|
|
55
|
-
return { collectionUrl, metaUrl: `${collectionUrl}/meta` };
|
|
56
|
-
}
|
|
57
36
|
/**
|
|
58
37
|
* Sends a signed Collection Metadata request, translating a 501
|
|
59
38
|
* `unsupported-operation` into a skip: Collection Metadata is an OPTIONAL
|
|
@@ -86,6 +65,31 @@ async function metaRequestOrSkip({ ctx, alice, url, method, json, headers }) {
|
|
|
86
65
|
throw err;
|
|
87
66
|
}
|
|
88
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Provisions a fresh Collection with a random id via a guarded (create-only)
|
|
70
|
+
* `PUT` of its Metadata object -- the v0.5 create-by-id path, now that `PUT`
|
|
71
|
+
* at the bare Collection URL is retired (405). Returns its container URL and
|
|
72
|
+
* its `/meta` URL. Every Collection Metadata test works on its own
|
|
73
|
+
* Collection, so the `metaVersion` sequence a test observes is its own.
|
|
74
|
+
*
|
|
75
|
+
* @param options {object}
|
|
76
|
+
* @param options.ctx {any} the test context (for `serverUrl` / `generateId` / `skip`)
|
|
77
|
+
* @param options.alice {any} the suite's root actor
|
|
78
|
+
* @returns {Promise<{collectionUrl: string, metaUrl: string}>}
|
|
79
|
+
*/
|
|
80
|
+
async function freshCollection({ ctx, alice }) {
|
|
81
|
+
const collectionId = ctx.generateId();
|
|
82
|
+
const collectionUrl = new URL(`/space/${alice.space1.id}/${collectionId}`, ctx.serverUrl).toString();
|
|
83
|
+
const metaUrl = `${collectionUrl}/meta`;
|
|
84
|
+
await metaRequestOrSkip({
|
|
85
|
+
ctx,
|
|
86
|
+
alice,
|
|
87
|
+
url: metaUrl,
|
|
88
|
+
method: 'PUT',
|
|
89
|
+
json: { id: collectionId, name: 'Metadata Collection' }
|
|
90
|
+
});
|
|
91
|
+
return { collectionUrl, metaUrl };
|
|
92
|
+
}
|
|
89
93
|
export const collectionApi = {
|
|
90
94
|
id: 'collection-api',
|
|
91
95
|
name: 'Collections API',
|
|
@@ -106,7 +110,7 @@ export const collectionApi = {
|
|
|
106
110
|
const { alice } = state;
|
|
107
111
|
try {
|
|
108
112
|
await alice.rootClient.request({
|
|
109
|
-
url: new URL(`/space/${alice.space1.id}
|
|
113
|
+
url: new URL(`/space/${alice.space1.id}/`, ctx.serverUrl).toString(),
|
|
110
114
|
method: 'DELETE'
|
|
111
115
|
});
|
|
112
116
|
}
|
|
@@ -140,12 +144,15 @@ export const collectionApi = {
|
|
|
140
144
|
const { serverUrl } = ctx;
|
|
141
145
|
const { alice } = state;
|
|
142
146
|
const spaceUrl = new URL('/space/space-id-that-does-not-exist/', serverUrl).toString();
|
|
147
|
+
// A body is required so the request reaches the existence check
|
|
148
|
+
// rather than failing body validation first.
|
|
143
149
|
let expectedError;
|
|
144
150
|
try {
|
|
145
151
|
await alice.rootClient.request({
|
|
146
152
|
url: spaceUrl,
|
|
147
153
|
method: 'POST',
|
|
148
|
-
action: 'POST'
|
|
154
|
+
action: 'POST',
|
|
155
|
+
json: { name: 'Probe Collection' }
|
|
149
156
|
});
|
|
150
157
|
}
|
|
151
158
|
catch (err) {
|
|
@@ -172,14 +179,17 @@ export const collectionApi = {
|
|
|
172
179
|
json: body
|
|
173
180
|
});
|
|
174
181
|
assert.equal(response.status, 201);
|
|
182
|
+
// The Collection's `url` is stamped in its canonical trailing-slash
|
|
183
|
+
// (container) form, matching a subsequent Read Collection Metadata.
|
|
175
184
|
assert.deepStrictEqual(withoutCreatedBy(response.data), {
|
|
176
185
|
id: 'credentials',
|
|
177
186
|
name: 'Verifiable Credentials',
|
|
178
187
|
type: ['Collection'],
|
|
179
|
-
backend: { id: 'default' }
|
|
188
|
+
backend: { id: 'default' },
|
|
189
|
+
url: `/space/${alice.space1.id}/credentials/`
|
|
180
190
|
});
|
|
181
191
|
assert.match(response.headers.get('content-type'), /application\/json/);
|
|
182
|
-
assert.equal(response.headers.get('location'), `${serverUrl}/space/${alice.space1.id}/${body.id}
|
|
192
|
+
assert.equal(response.headers.get('location'), `${serverUrl}/space/${alice.space1.id}/${body.id}/`);
|
|
183
193
|
}
|
|
184
194
|
},
|
|
185
195
|
{
|
|
@@ -221,7 +231,9 @@ export const collectionApi = {
|
|
|
221
231
|
assert.equal(response.status, 200);
|
|
222
232
|
const listResponse = response.data;
|
|
223
233
|
assert.equal(listResponse.id, 'credentials');
|
|
224
|
-
|
|
234
|
+
// The listing envelope's own `url` carries the trailing slash (spec
|
|
235
|
+
// "Reading This Document": a trailing slash marks a container).
|
|
236
|
+
assert.equal(listResponse.url, `/space/${alice.space1.id}/credentials/`);
|
|
225
237
|
assert.equal(listResponse.name, 'Verifiable Credentials');
|
|
226
238
|
assert.deepStrictEqual(listResponse.type, ['Collection']);
|
|
227
239
|
assert.equal(typeof listResponse.totalItems, 'number');
|
|
@@ -230,30 +242,98 @@ export const collectionApi = {
|
|
|
230
242
|
}
|
|
231
243
|
},
|
|
232
244
|
{
|
|
233
|
-
id: 'collection.read-
|
|
234
|
-
name: '[root] get
|
|
245
|
+
id: 'collection.read-metadata',
|
|
246
|
+
name: '[root] get the Collection Metadata object via GET :collectionId/meta',
|
|
235
247
|
specRefs: [
|
|
236
|
-
'https://wallet.storage/spec#
|
|
248
|
+
'https://wallet.storage/spec#read-collection-metadata-operation',
|
|
249
|
+
'https://wallet.storage/spec#collection-metadata-data-model'
|
|
237
250
|
],
|
|
238
251
|
run: async (ctx, state) => {
|
|
239
252
|
const { serverUrl, withoutCreatedBy } = ctx;
|
|
240
253
|
const { alice } = state;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
// v0.5 retires the bare-URL Collection description; reading a
|
|
255
|
+
// Collection's description is now always a GET of `meta`.
|
|
256
|
+
const response = await metaRequestOrSkip({
|
|
257
|
+
ctx,
|
|
258
|
+
alice,
|
|
259
|
+
url: new URL(`/space/${alice.space1.id}/credentials/meta`, serverUrl).toString(),
|
|
260
|
+
method: 'GET'
|
|
245
261
|
});
|
|
246
262
|
assert.equal(response.status, 200);
|
|
247
|
-
|
|
263
|
+
const { createdAt, updatedAt, ...rest } = withoutCreatedBy(response.data);
|
|
264
|
+
assert.ok(!Number.isNaN(Date.parse(createdAt)));
|
|
265
|
+
assert.ok(!Number.isNaN(Date.parse(updatedAt)));
|
|
266
|
+
assert.deepStrictEqual(rest, {
|
|
248
267
|
id: 'credentials',
|
|
249
268
|
name: 'Verifiable Credentials',
|
|
250
269
|
type: ['Collection'],
|
|
251
270
|
backend: { id: 'default' },
|
|
252
|
-
url: `/space/${alice.space1.id}/credentials
|
|
271
|
+
url: `/space/${alice.space1.id}/credentials/`,
|
|
253
272
|
linkset: `/space/${alice.space1.id}/credentials/linkset`
|
|
254
273
|
});
|
|
255
274
|
}
|
|
256
275
|
},
|
|
276
|
+
{
|
|
277
|
+
id: 'collection.non-canonical-url-redirects',
|
|
278
|
+
name: '[root] the non-canonical (no-slash) Collection URL 308s to the ' +
|
|
279
|
+
'canonical (trailing-slash) form; the canonical form is not redirected',
|
|
280
|
+
optional: true,
|
|
281
|
+
specRefs: ['https://wallet.storage/spec#reading-this-document'],
|
|
282
|
+
run: async (ctx, state) => {
|
|
283
|
+
const { serverUrl } = ctx;
|
|
284
|
+
const { alice } = state;
|
|
285
|
+
const bareUrl = new URL(`/space/${alice.space1.id}/credentials`, serverUrl);
|
|
286
|
+
const canonicalPath = `/space/${alice.space1.id}/credentials/`;
|
|
287
|
+
const redirected = await fetch(bareUrl, {
|
|
288
|
+
method: 'GET',
|
|
289
|
+
redirect: 'manual'
|
|
290
|
+
});
|
|
291
|
+
if (redirected.type === 'opaqueredirect') {
|
|
292
|
+
ctx.skip('redirect responses are opaque in this runtime (cross-origin fetch)');
|
|
293
|
+
}
|
|
294
|
+
assert.equal(redirected.status, 308);
|
|
295
|
+
const location = redirected.headers.get('location');
|
|
296
|
+
assert.ok(location, 'expected a Location header on the redirect');
|
|
297
|
+
assert.equal(new URL(location, serverUrl).pathname, canonicalPath);
|
|
298
|
+
// The canonical form itself is never redirected -- whatever it
|
|
299
|
+
// answers (200, 401, 404...) it is not a 308.
|
|
300
|
+
const canonical = await fetch(new URL(canonicalPath, serverUrl), {
|
|
301
|
+
method: 'GET',
|
|
302
|
+
redirect: 'manual'
|
|
303
|
+
});
|
|
304
|
+
assert.notEqual(canonical.status, 308);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 'collection.put-container-405',
|
|
309
|
+
name: '[root] PUT at the canonical Collection URL is 405 (PUT is not ' +
|
|
310
|
+
'defined at the container)',
|
|
311
|
+
specRefs: ['https://wallet.storage/spec#collection-metadata-data-model'],
|
|
312
|
+
run: async (ctx, state) => {
|
|
313
|
+
const { serverUrl } = ctx;
|
|
314
|
+
const { alice } = state;
|
|
315
|
+
let expectedError;
|
|
316
|
+
try {
|
|
317
|
+
await alice.rootClient.request({
|
|
318
|
+
url: new URL(`/space/${alice.space1.id}/credentials/`, serverUrl).toString(),
|
|
319
|
+
method: 'PUT',
|
|
320
|
+
action: 'PUT',
|
|
321
|
+
json: { name: 'Should Not Replace' }
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
expectedError = err;
|
|
326
|
+
}
|
|
327
|
+
assert.ok(expectedError, 'expected a PUT at the Collection container URL to be refused');
|
|
328
|
+
assert.equal(expectedError.response.status, 405);
|
|
329
|
+
const allow = (expectedError.response.headers.get('allow') ?? '')
|
|
330
|
+
.split(',')
|
|
331
|
+
.map((method) => method.trim())
|
|
332
|
+
.filter(Boolean);
|
|
333
|
+
assert.ok(!allow.includes('PUT'), 'Allow must not include PUT');
|
|
334
|
+
assert.match(expectedError.response.headers.get('content-type'), /application\/problem\+json/);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
257
337
|
{
|
|
258
338
|
id: 'collection.paginate-limit-next',
|
|
259
339
|
name: '[root] paginates List Collection via ?limit and follows next (spec Pagination)',
|
|
@@ -267,9 +347,12 @@ export const collectionApi = {
|
|
|
267
347
|
// Fresh Collection seeded with > one page of Resources, inserted out of order
|
|
268
348
|
// to prove the listing order is by id, not insertion.
|
|
269
349
|
const collectionId = generateId();
|
|
270
|
-
// WAS does not auto-create parent Collections, so provision it first
|
|
271
|
-
|
|
272
|
-
|
|
350
|
+
// WAS does not auto-create parent Collections, so provision it first
|
|
351
|
+
// (create-by-id is a guarded `PUT` of its Metadata object).
|
|
352
|
+
await metaRequestOrSkip({
|
|
353
|
+
ctx,
|
|
354
|
+
alice,
|
|
355
|
+
url: new URL(`/space/${alice.space1.id}/${collectionId}/meta`, serverUrl).toString(),
|
|
273
356
|
method: 'PUT',
|
|
274
357
|
json: { id: collectionId, name: 'Paginated Collection' }
|
|
275
358
|
});
|
|
@@ -337,25 +420,30 @@ export const collectionApi = {
|
|
|
337
420
|
const { alice } = state;
|
|
338
421
|
const collectionId = 'new-collection';
|
|
339
422
|
const collectionUrl = new URL(`/space/${alice.space1.id}/${collectionId}`, serverUrl).toString();
|
|
423
|
+
const metaUrl = `${collectionUrl}/meta`;
|
|
340
424
|
const body = { id: collectionId, name: 'New Collection' };
|
|
341
|
-
await
|
|
342
|
-
|
|
425
|
+
const created = await metaRequestOrSkip({
|
|
426
|
+
ctx,
|
|
427
|
+
alice,
|
|
428
|
+
url: metaUrl,
|
|
343
429
|
method: 'PUT',
|
|
344
430
|
json: body
|
|
345
431
|
});
|
|
432
|
+
assert.equal(created.status, 201);
|
|
346
433
|
const existResponse = await alice.rootClient.request({
|
|
347
|
-
url:
|
|
434
|
+
url: metaUrl,
|
|
348
435
|
method: 'GET'
|
|
349
436
|
});
|
|
350
437
|
assert.equal(existResponse.status, 200);
|
|
438
|
+
// Delete Collection is at the canonical (trailing-slash) container URL.
|
|
351
439
|
const deleteResponse = await alice.rootClient.request({
|
|
352
|
-
url: collectionUrl
|
|
440
|
+
url: `${collectionUrl}/`,
|
|
353
441
|
method: 'DELETE'
|
|
354
442
|
});
|
|
355
443
|
assert.equal(deleteResponse.status, 204);
|
|
356
444
|
let checkResponse;
|
|
357
445
|
try {
|
|
358
|
-
await alice.rootClient.request({ url:
|
|
446
|
+
await alice.rootClient.request({ url: metaUrl, method: 'GET' });
|
|
359
447
|
}
|
|
360
448
|
catch (err) {
|
|
361
449
|
checkResponse = err.response;
|
|
@@ -398,6 +486,37 @@ export const collectionApi = {
|
|
|
398
486
|
assert.equal(expectedError.data.type, 'https://wallet.storage/spec#unsupported-backend');
|
|
399
487
|
}
|
|
400
488
|
},
|
|
489
|
+
{
|
|
490
|
+
id: 'collection.create-post-meta-reserved-id-409',
|
|
491
|
+
name: '[root] creating a Collection with the reserved id `meta` via POST ' +
|
|
492
|
+
'is rejected with 409 reserved-id',
|
|
493
|
+
specRefs: [
|
|
494
|
+
'https://wallet.storage/spec#space-level-reserved-endpoints',
|
|
495
|
+
'https://wallet.storage/spec#reserved-id'
|
|
496
|
+
],
|
|
497
|
+
run: async (ctx, state) => {
|
|
498
|
+
const { serverUrl } = ctx;
|
|
499
|
+
const { alice } = state;
|
|
500
|
+
// `meta` occupies the Space Metadata object's own path
|
|
501
|
+
// (`/space/{s}/meta`), so it is a reserved Collection id -- v0.5
|
|
502
|
+
// widens the Space-level reserved-endpoint table to include it.
|
|
503
|
+
let expectedError;
|
|
504
|
+
try {
|
|
505
|
+
await alice.rootClient.request({
|
|
506
|
+
url: new URL(`/space/${alice.space1.id}/`, serverUrl).toString(),
|
|
507
|
+
method: 'POST',
|
|
508
|
+
action: 'POST',
|
|
509
|
+
json: { id: 'meta', name: 'Reserved Meta Collection Id Probe' }
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
expectedError = err;
|
|
514
|
+
}
|
|
515
|
+
assert.ok(expectedError, 'expected the `meta` Collection id to be rejected');
|
|
516
|
+
assert.equal(expectedError.response.status, 409);
|
|
517
|
+
assert.equal(expectedError.data.type, 'https://wallet.storage/spec#reserved-id');
|
|
518
|
+
}
|
|
519
|
+
},
|
|
401
520
|
{
|
|
402
521
|
id: 'collection.paginate-delegated-no-redelegation',
|
|
403
522
|
name: '[delegated] a single list capability reads every page; no per-page ' +
|
|
@@ -412,8 +531,10 @@ export const collectionApi = {
|
|
|
412
531
|
const aliceDelegatedApp = ctx.actors.aliceDelegatedApp;
|
|
413
532
|
// Seed a fresh Collection with more than one page of Resources.
|
|
414
533
|
const collectionId = generateId();
|
|
415
|
-
await
|
|
416
|
-
|
|
534
|
+
await metaRequestOrSkip({
|
|
535
|
+
ctx,
|
|
536
|
+
alice,
|
|
537
|
+
url: new URL(`/space/${alice.space1.id}/${collectionId}/meta`, serverUrl).toString(),
|
|
417
538
|
method: 'PUT',
|
|
418
539
|
json: { id: collectionId, name: 'Delegated Paginated Collection' }
|
|
419
540
|
});
|
|
@@ -501,7 +622,7 @@ export const collectionApi = {
|
|
|
501
622
|
name: '[root] PUT Collection Metadata sets `custom`, round-tripped by GET',
|
|
502
623
|
group: 'Collection Metadata',
|
|
503
624
|
specRefs: [
|
|
504
|
-
'https://wallet.storage/spec#update-collection-
|
|
625
|
+
'https://wallet.storage/spec#update-or-create-by-id-collection-operation',
|
|
505
626
|
'https://wallet.storage/spec#read-collection-metadata-operation'
|
|
506
627
|
],
|
|
507
628
|
run: async (ctx, state) => {
|
|
@@ -538,7 +659,7 @@ export const collectionApi = {
|
|
|
538
659
|
'`custom` clears it',
|
|
539
660
|
group: 'Collection Metadata',
|
|
540
661
|
specRefs: [
|
|
541
|
-
'https://wallet.storage/spec#update-collection-
|
|
662
|
+
'https://wallet.storage/spec#update-or-create-by-id-collection-operation'
|
|
542
663
|
],
|
|
543
664
|
run: async (ctx, state) => {
|
|
544
665
|
const { alice } = state;
|
|
@@ -575,7 +696,7 @@ export const collectionApi = {
|
|
|
575
696
|
'members (read-modify-write is safe)',
|
|
576
697
|
group: 'Collection Metadata',
|
|
577
698
|
specRefs: [
|
|
578
|
-
'https://wallet.storage/spec#update-collection-
|
|
699
|
+
'https://wallet.storage/spec#update-or-create-by-id-collection-operation',
|
|
579
700
|
'https://wallet.storage/spec#collection-metadata-data-model'
|
|
580
701
|
],
|
|
581
702
|
run: async (ctx, state) => {
|
|
@@ -619,37 +740,39 @@ export const collectionApi = {
|
|
|
619
740
|
}
|
|
620
741
|
},
|
|
621
742
|
{
|
|
622
|
-
id: 'collection.meta-put-missing-collection
|
|
623
|
-
name: '[root] PUT Collection Metadata on a nonexistent Collection
|
|
624
|
-
'(
|
|
743
|
+
id: 'collection.meta-put-creates-missing-collection',
|
|
744
|
+
name: '[root] PUT Collection Metadata on a nonexistent Collection creates ' +
|
|
745
|
+
'it (201), the `Location` naming the Collection',
|
|
625
746
|
group: 'Collection Metadata',
|
|
626
747
|
specRefs: [
|
|
627
|
-
'https://wallet.storage/spec#update-collection-
|
|
628
|
-
'https://wallet.storage/spec#not-found'
|
|
748
|
+
'https://wallet.storage/spec#update-or-create-by-id-collection-operation'
|
|
629
749
|
],
|
|
630
750
|
run: async (ctx, state) => {
|
|
631
751
|
const { serverUrl } = ctx;
|
|
632
752
|
const { alice } = state;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
await metaRequestOrSkip({
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
assert.equal(
|
|
652
|
-
assert.
|
|
753
|
+
const collectionId = ctx.generateId();
|
|
754
|
+
const collectionUrl = new URL(`/space/${alice.space1.id}/${collectionId}`, serverUrl).toString();
|
|
755
|
+
const metaUrl = `${collectionUrl}/meta`;
|
|
756
|
+
const response = await metaRequestOrSkip({
|
|
757
|
+
ctx,
|
|
758
|
+
alice,
|
|
759
|
+
url: metaUrl,
|
|
760
|
+
method: 'PUT',
|
|
761
|
+
json: { custom: { name: 'Created via meta' } }
|
|
762
|
+
});
|
|
763
|
+
assert.equal(response.status, 201);
|
|
764
|
+
// `Location` names the Collection created, in its canonical
|
|
765
|
+
// trailing-slash form -- not the Metadata object that was written.
|
|
766
|
+
assert.equal(response.headers.get('location'), `${collectionUrl}/`);
|
|
767
|
+
const created = await alice.rootClient.request({
|
|
768
|
+
url: metaUrl,
|
|
769
|
+
method: 'GET'
|
|
770
|
+
});
|
|
771
|
+
assert.equal(created.status, 200);
|
|
772
|
+
assert.equal(created.data.id, collectionId);
|
|
773
|
+
assert.deepStrictEqual(created.data.custom, {
|
|
774
|
+
name: 'Created via meta'
|
|
775
|
+
});
|
|
653
776
|
}
|
|
654
777
|
},
|
|
655
778
|
{
|
|
@@ -658,7 +781,7 @@ export const collectionApi = {
|
|
|
658
781
|
'rejected with 400 invalid-request-body',
|
|
659
782
|
group: 'Collection Metadata',
|
|
660
783
|
specRefs: [
|
|
661
|
-
'https://wallet.storage/spec#update-collection-
|
|
784
|
+
'https://wallet.storage/spec#update-or-create-by-id-collection-operation',
|
|
662
785
|
'https://wallet.storage/spec#invalid-request-body'
|
|
663
786
|
],
|
|
664
787
|
run: async (ctx, state) => {
|
|
@@ -688,7 +811,7 @@ export const collectionApi = {
|
|
|
688
811
|
'one succeeds',
|
|
689
812
|
group: 'Collection Metadata',
|
|
690
813
|
specRefs: [
|
|
691
|
-
'https://wallet.storage/spec#update-collection-
|
|
814
|
+
'https://wallet.storage/spec#update-or-create-by-id-collection-operation',
|
|
692
815
|
'https://wallet.storage/spec#conditional-requests',
|
|
693
816
|
'https://wallet.storage/spec#precondition-failed'
|
|
694
817
|
],
|
|
@@ -745,78 +868,74 @@ export const collectionApi = {
|
|
|
745
868
|
}
|
|
746
869
|
},
|
|
747
870
|
{
|
|
748
|
-
id: 'collection.meta-
|
|
749
|
-
name: '[root]
|
|
750
|
-
'
|
|
871
|
+
id: 'collection.meta-configuration-and-annotation-share-etag',
|
|
872
|
+
name: '[root] a configuration write and an annotation write on Collection ' +
|
|
873
|
+
'Metadata advance the same ETag',
|
|
751
874
|
group: 'Collection Metadata',
|
|
752
875
|
specRefs: [
|
|
753
|
-
'https://wallet.storage/spec#collection-metadata-
|
|
876
|
+
'https://wallet.storage/spec#collection-metadata-versioning',
|
|
754
877
|
'https://wallet.storage/spec#conditional-requests'
|
|
755
878
|
],
|
|
756
879
|
run: async (ctx, state) => {
|
|
757
880
|
const { alice } = state;
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
ctx.skip('server does not emit an ETag on the Collection Description');
|
|
766
|
-
}
|
|
767
|
-
// A metadata write does not disturb the description validator...
|
|
768
|
-
await metaRequestOrSkip({
|
|
881
|
+
// v0.5 merges what used to be two independent validators (the
|
|
882
|
+
// Collection description's and the `/meta` annotation object's) into
|
|
883
|
+
// the one Collection Metadata object's `metaVersion`, so a
|
|
884
|
+
// configuration write and an annotation write now advance the SAME
|
|
885
|
+
// ETag rather than independent ones.
|
|
886
|
+
const { metaUrl } = await freshCollection({ ctx, alice });
|
|
887
|
+
const created = await metaRequestOrSkip({
|
|
769
888
|
ctx,
|
|
770
889
|
alice,
|
|
771
890
|
url: metaUrl,
|
|
772
|
-
method: 'PUT',
|
|
773
|
-
json: { custom: { name: 'Independent' } }
|
|
774
|
-
});
|
|
775
|
-
const afterMeta = await alice.rootClient.request({
|
|
776
|
-
url: collectionUrl,
|
|
777
891
|
method: 'GET'
|
|
778
892
|
});
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
//
|
|
782
|
-
await alice.rootClient.request({
|
|
783
|
-
url:
|
|
893
|
+
const createdEtag = created.headers.get('etag');
|
|
894
|
+
assert.ok(createdEtag, 'expected an ETag on Collection Metadata');
|
|
895
|
+
// An annotation write advances the one validator...
|
|
896
|
+
const annotated = await alice.rootClient.request({
|
|
897
|
+
url: metaUrl,
|
|
784
898
|
method: 'PUT',
|
|
785
899
|
action: 'PUT',
|
|
786
|
-
json: {
|
|
787
|
-
id: described.data.id,
|
|
788
|
-
type: ['Collection'],
|
|
789
|
-
name: 'Renamed'
|
|
790
|
-
}
|
|
900
|
+
json: { name: 'Metadata Collection', custom: { name: 'Shared' } }
|
|
791
901
|
});
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
902
|
+
const annotatedEtag = annotated.headers.get('etag');
|
|
903
|
+
assert.notEqual(annotatedEtag, createdEtag);
|
|
904
|
+
// ...and so does a configuration write (renaming the plaintext
|
|
905
|
+
// `name` is a configuration member, not an annotation).
|
|
906
|
+
const configured = await alice.rootClient.request({
|
|
907
|
+
url: metaUrl,
|
|
908
|
+
method: 'PUT',
|
|
909
|
+
action: 'PUT',
|
|
910
|
+
json: { name: 'Renamed Collection', custom: { name: 'Shared' } }
|
|
795
911
|
});
|
|
796
|
-
|
|
797
|
-
|
|
912
|
+
const configuredEtag = configured.headers.get('etag');
|
|
913
|
+
assert.notEqual(configuredEtag, annotatedEtag);
|
|
914
|
+
const meta = await alice.rootClient.request({
|
|
798
915
|
url: metaUrl,
|
|
799
916
|
method: 'GET'
|
|
800
917
|
});
|
|
801
|
-
assert.equal(
|
|
802
|
-
assert.
|
|
918
|
+
assert.equal(meta.headers.get('etag'), configuredEtag);
|
|
919
|
+
assert.equal(meta.data.name, 'Renamed Collection');
|
|
920
|
+
assert.deepStrictEqual(meta.data.custom, { name: 'Shared' });
|
|
803
921
|
}
|
|
804
922
|
},
|
|
805
923
|
{
|
|
806
|
-
id: 'collection.meta-reserved-
|
|
807
|
-
name: '[root]
|
|
808
|
-
'
|
|
924
|
+
id: 'collection.meta-delete-405-not-reserved-id',
|
|
925
|
+
name: '[root] DELETE at the Collection Metadata URL is 405 with an Allow ' +
|
|
926
|
+
'header, not a reserved-id 409',
|
|
809
927
|
group: 'Collection Metadata',
|
|
810
928
|
specRefs: [
|
|
811
|
-
'https://wallet.storage/spec#
|
|
812
|
-
'https://wallet.storage/spec#reserved-
|
|
929
|
+
'https://wallet.storage/spec#collection-metadata-data-model',
|
|
930
|
+
'https://wallet.storage/spec#methods-at-reserved-endpoints'
|
|
813
931
|
],
|
|
814
932
|
run: async (ctx, state) => {
|
|
815
933
|
const { alice } = state;
|
|
816
|
-
//
|
|
817
|
-
// a Resource
|
|
818
|
-
//
|
|
819
|
-
//
|
|
934
|
+
// `meta` occupies the Collection's `:resourceId` position, so `meta`
|
|
935
|
+
// is a reserved Resource id -- but there is no DELETE defined at
|
|
936
|
+
// `meta` (spec "Lifecycle": deleting the Collection removes its
|
|
937
|
+
// Metadata object with it), so v0.5's Methods at Reserved Endpoints
|
|
938
|
+
// rule answers 405 here, never the reserved-id 409.
|
|
820
939
|
const { metaUrl } = await freshCollection({ ctx, alice });
|
|
821
940
|
await metaRequestOrSkip({ ctx, alice, url: metaUrl, method: 'GET' });
|
|
822
941
|
let expectedError;
|
|
@@ -830,12 +949,84 @@ export const collectionApi = {
|
|
|
830
949
|
catch (err) {
|
|
831
950
|
expectedError = err;
|
|
832
951
|
}
|
|
833
|
-
assert.ok(expectedError, 'expected a
|
|
834
|
-
|
|
835
|
-
|
|
952
|
+
assert.ok(expectedError, 'expected a DELETE at the Collection Metadata URL to be refused');
|
|
953
|
+
assert.equal(expectedError.response.status, 405);
|
|
954
|
+
const allow = (expectedError.response.headers.get('allow') ?? '')
|
|
955
|
+
.split(',')
|
|
956
|
+
.map((method) => method.trim())
|
|
957
|
+
.filter(Boolean);
|
|
958
|
+
assert.ok(!allow.includes('DELETE'), 'Allow must not include DELETE');
|
|
959
|
+
assert.equal(expectedError.data.type, 'about:blank');
|
|
960
|
+
assert.notEqual(expectedError.data.type, 'https://wallet.storage/spec#reserved-id');
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
id: 'collection.meta-update-omits-backend-keeps-selection',
|
|
965
|
+
name: '[root] a Collection Metadata update omitting `backend` keeps the ' +
|
|
966
|
+
'stored selection',
|
|
967
|
+
group: 'Collection Metadata',
|
|
968
|
+
// Proving retention (rather than a no-op default-to-default) needs a
|
|
969
|
+
// genuinely non-default backend, which requires the reference server's
|
|
970
|
+
// backend-registration write endpoint -- a facility the spec does not
|
|
971
|
+
// define the wire contract of (only `GET .../backends` is spec'd).
|
|
972
|
+
// Skip gracefully wherever that facility, or Collection Metadata
|
|
973
|
+
// itself, is unavailable.
|
|
974
|
+
optional: true,
|
|
975
|
+
specRefs: [
|
|
976
|
+
'https://wallet.storage/spec#update-or-create-by-id-collection-operation',
|
|
977
|
+
'https://wallet.storage/spec#collection-metadata-data-model'
|
|
978
|
+
],
|
|
979
|
+
run: async (ctx, state) => {
|
|
980
|
+
const { serverUrl, generateId } = ctx;
|
|
981
|
+
const { alice } = state;
|
|
982
|
+
const backendId = generateId();
|
|
983
|
+
try {
|
|
984
|
+
await alice.rootClient.request({
|
|
985
|
+
url: new URL(`/space/${alice.space1.id}/backends`, serverUrl).toString(),
|
|
986
|
+
method: 'POST',
|
|
987
|
+
action: 'POST',
|
|
988
|
+
json: {
|
|
989
|
+
id: backendId,
|
|
990
|
+
provider: 'google-drive',
|
|
991
|
+
connection: {
|
|
992
|
+
kind: 'oauth2-google',
|
|
993
|
+
authorizationCode: 'probe-code'
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
836
997
|
}
|
|
837
|
-
|
|
838
|
-
|
|
998
|
+
catch {
|
|
999
|
+
ctx.skip('server does not support registering a non-default backend ' +
|
|
1000
|
+
'(reference-server extension, not spec-defined)');
|
|
1001
|
+
}
|
|
1002
|
+
const collectionId = generateId();
|
|
1003
|
+
const collectionUrl = new URL(`/space/${alice.space1.id}/${collectionId}`, serverUrl).toString();
|
|
1004
|
+
await alice.rootClient.request({
|
|
1005
|
+
url: new URL(`/space/${alice.space1.id}/`, serverUrl).toString(),
|
|
1006
|
+
method: 'POST',
|
|
1007
|
+
action: 'POST',
|
|
1008
|
+
json: {
|
|
1009
|
+
id: collectionId,
|
|
1010
|
+
name: 'Backend Retention Probe',
|
|
1011
|
+
backend: { id: backendId }
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
const metaUrl = `${collectionUrl}/meta`;
|
|
1015
|
+
// An update that omits `backend` must keep the stored selection, not
|
|
1016
|
+
// reset it to the default (spec "Update Collection": "Clearing it
|
|
1017
|
+
// would repoint the Collection at the default backend").
|
|
1018
|
+
await metaRequestOrSkip({
|
|
1019
|
+
ctx,
|
|
1020
|
+
alice,
|
|
1021
|
+
url: metaUrl,
|
|
1022
|
+
method: 'PUT',
|
|
1023
|
+
json: { name: 'Renamed, backend omitted' }
|
|
1024
|
+
});
|
|
1025
|
+
const after = await alice.rootClient.request({
|
|
1026
|
+
url: metaUrl,
|
|
1027
|
+
method: 'GET'
|
|
1028
|
+
});
|
|
1029
|
+
assert.deepStrictEqual(after.data.backend, { id: backendId });
|
|
839
1030
|
}
|
|
840
1031
|
}
|
|
841
1032
|
]
|