@stowage/conformance 0.1.0 → 0.2.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 +20 -17
- package/dist/index.d.ts +2 -2
- package/dist/index.js +253 -53
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install --save-dev @stowage/conformance
|
|
|
14
14
|
A `ConformanceTarget` tells the suite how to construct the storage under test. `createStorage`
|
|
15
15
|
returns a storage the run may write to below any prefix, constructed from outside the adapter the
|
|
16
16
|
way a caller would construct it
|
|
17
|
-
([spec
|
|
17
|
+
([spec 9.3](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#93-what-the-target-promises)).
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
import { memoryStorage } from "@stowage/adapter-memory";
|
|
@@ -41,14 +41,15 @@ The optional members widen the run:
|
|
|
41
41
|
The suite reads what the storage supports from its `capabilities`, once per run, before the first
|
|
42
42
|
case. The storage lists every name out of `capabilityNames` it implements, each once, and the list
|
|
43
43
|
is fixed when it is constructed
|
|
44
|
-
([spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.
|
|
44
|
+
([spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#49-capabilities)).
|
|
45
45
|
Every storage the target creates in one run declares the same; two configurations are two targets.
|
|
46
46
|
|
|
47
|
-
A case whose `requires` the storage declares runs its `run` half. A case
|
|
48
|
-
the `runWithout` half, which checks what the storage does without the capability:
|
|
49
|
-
call with `Unsupported` naming it, `presignedUrls` has `presignGet` and
|
|
50
|
-
storage, and a few check the weaker behavior, such as a copy whose
|
|
51
|
-
|
|
47
|
+
A case whose `requires` the storage declares runs its `run` half. A case missing a name of its
|
|
48
|
+
`requires` runs the `runWithout` half, which checks what the storage does without the capability:
|
|
49
|
+
most refuse the call with `Unsupported` naming it, `presignedUrls` has `presignGet` and
|
|
50
|
+
`presignPut` absent from the storage, and a few check the weaker behavior, such as a copy whose
|
|
51
|
+
destination reads `{}`
|
|
52
|
+
([spec 9.5](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#95-cases)).
|
|
52
53
|
An adapter refuses such a call like this:
|
|
53
54
|
|
|
54
55
|
```ts
|
|
@@ -73,7 +74,7 @@ export function refuseUserMetadata(bucket: string): StorageError {
|
|
|
73
74
|
|
|
74
75
|
`describeConformance(target, framework)` maps every case onto the `describe` and `test` of the
|
|
75
76
|
framework it is handed
|
|
76
|
-
([spec
|
|
77
|
+
([spec 9.2](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#92-running-the-suite)).
|
|
77
78
|
It runs the `fast` cases, and both tiers with `includeSlow: true`.
|
|
78
79
|
|
|
79
80
|
Vitest:
|
|
@@ -156,27 +157,29 @@ A failed result carries the error as `name`, `message`, `stack` and, for a `Stor
|
|
|
156
157
|
|
|
157
158
|
## Read an adapter
|
|
158
159
|
|
|
159
|
-
[`@stowage/adapter-memory`](https://github.com/stowage-js/stowage/tree/@stowage/adapter-memory@0.
|
|
160
|
-
is the implementation a third-party adapter is read against
|
|
161
|
-
|
|
160
|
+
[`@stowage/adapter-memory`](https://github.com/stowage-js/stowage/tree/@stowage/adapter-memory@0.2.0/packages/adapter-memory/src)
|
|
161
|
+
is the implementation a third-party adapter is read against, out of the four adapters of this
|
|
162
|
+
repository that pass these cases: `@stowage/adapter-memory`, `@stowage/adapter-fs`,
|
|
163
|
+
`@stowage/adapter-s3` and `@stowage/adapter-azure-blob`. It enforces the key rule exactly,
|
|
164
|
+
declares four of the five capabilities, and has no network in the way. The cases themselves are
|
|
162
165
|
written against the behavior of S3, not against it.
|
|
163
166
|
|
|
164
167
|
What the suite leaves to an adapter's own tests, such as flat memory during a large upload or the
|
|
165
168
|
retry curve, is listed in
|
|
166
|
-
[spec
|
|
169
|
+
[spec 9.4](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#94-what-the-suite-does-not-assert).
|
|
167
170
|
|
|
168
171
|
## Runtimes
|
|
169
172
|
|
|
170
|
-
Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01
|
|
173
|
+
Node 24 and later, Bun, Deno and `workerd` at the compatibility date `2026-09-01` without Node APIs. CI last ran green on Bun 1.4.2 and Deno 2.9.6.
|
|
171
174
|
|
|
172
|
-
The bundle measures
|
|
175
|
+
The bundle measures 11.6 kB minified and gzipped, `@stowage/core` included.
|
|
173
176
|
|
|
174
177
|
## Specification
|
|
175
178
|
|
|
176
|
-
[`docs/spec.md` at `@stowage/conformance@0.
|
|
179
|
+
[`docs/spec.md` at `@stowage/conformance@0.2.0`](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#9-stowageconformance)
|
|
177
180
|
is the contract: a caller may rely on what it states and on nothing else this package happens to
|
|
178
|
-
export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.
|
|
179
|
-
and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/conformance@0.
|
|
181
|
+
export. The [terms it uses](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/CONTEXT.md)
|
|
182
|
+
and the [decisions behind it](https://github.com/stowage-js/stowage/tree/@stowage/conformance@0.2.0/docs/adr)
|
|
180
183
|
are at the same tag.
|
|
181
184
|
|
|
182
185
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { CapabilityName, Storage, StorageErrorCode } from "@stowage/core";
|
|
|
2
2
|
//#region src/assertions.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Spec 4.9 leaves a call needing a capability the storage does not declare with an
|
|
5
|
-
* `Unsupported` error naming it, and spec
|
|
5
|
+
* `Unsupported` error naming it, and spec 9.2 has most of the `runWithout` halves assert
|
|
6
6
|
* that through this.
|
|
7
7
|
*/
|
|
8
8
|
export declare function expectUnsupported(call: () => Promise<unknown>, capability: CapabilityName): Promise<void>;
|
|
@@ -43,7 +43,7 @@ interface ConformanceCaseMetadata {
|
|
|
43
43
|
}
|
|
44
44
|
//#endregion
|
|
45
45
|
//#region src/cases/index.d.ts
|
|
46
|
-
/** The same cases as the package publishes them, without the factory of spec
|
|
46
|
+
/** The same cases as the package publishes them, without the factory of spec 9.3. */
|
|
47
47
|
export declare const conformanceCases: readonly ConformanceCase[];
|
|
48
48
|
//#endregion
|
|
49
49
|
//#region src/result.d.ts
|
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ function assertDate(value, what) {
|
|
|
61
61
|
assert(value instanceof Date && Number.isFinite(value.getTime()), `${what} is ${JSON.stringify(value)}, which is no \`Date\``);
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
* Two descriptions of one object, on the fields the row of spec
|
|
64
|
+
* Two descriptions of one object, on the fields the row of spec 9.5 names. Spec 4.4
|
|
65
65
|
* leaves `lastModified` free to differ between a write and a later read by the
|
|
66
66
|
* provider's rounding, so a row names its fields rather than the whole description.
|
|
67
67
|
*/
|
|
@@ -69,7 +69,7 @@ function assertSameDescription(one, other, fields, what) {
|
|
|
69
69
|
for (const field of fields) assert(one[field] === other[field], `${what} report \`${field}\` as ${JSON.stringify(one[field])} and ${JSON.stringify(other[field])}`);
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
|
-
* A header of the answer a case read, which is how spec
|
|
72
|
+
* A header of the answer a case read, which is how spec 9.5 and 9.6 have the two cases
|
|
73
73
|
* that leave the API — a signed URL and the `Response` of flow 4 — read a content type.
|
|
74
74
|
*/
|
|
75
75
|
function assertHeader(response, name, expected) {
|
|
@@ -78,7 +78,7 @@ function assertHeader(response, name, expected) {
|
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* The `StorageError` the call is expected to reject with, handed back so that a case can
|
|
81
|
-
* read it for what its row of spec
|
|
81
|
+
* read it for what its row of spec 9.5 states beyond the fields it named here. A case
|
|
82
82
|
* running the same call over a list of keys names the one it is at in `what`.
|
|
83
83
|
*/
|
|
84
84
|
async function expectStorageError(call, expected, what) {
|
|
@@ -114,7 +114,7 @@ async function expectRuntimeError(call, name) {
|
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* Spec 4.9 leaves a call needing a capability the storage does not declare with an
|
|
117
|
-
* `Unsupported` error naming it, and spec
|
|
117
|
+
* `Unsupported` error naming it, and spec 9.2 has most of the `runWithout` halves assert
|
|
118
118
|
* that through this.
|
|
119
119
|
*/
|
|
120
120
|
async function expectUnsupported(call, capability) {
|
|
@@ -138,7 +138,7 @@ async function rejectionOf(call, expectation) {
|
|
|
138
138
|
//#endregion
|
|
139
139
|
//#region src/cases/keys.ts
|
|
140
140
|
const utf8$6 = new TextEncoder();
|
|
141
|
-
/** Spec
|
|
141
|
+
/** Spec 9.7 measures a key in UTF-8 bytes, and `adapter-fs` bounds one segment at 255. */
|
|
142
142
|
const segmentLimit = 255;
|
|
143
143
|
/**
|
|
144
144
|
* The characters spec 4.8 refuses anywhere in a key, at both ends of the range it names
|
|
@@ -150,6 +150,11 @@ const refusedControlCharacters = [
|
|
|
150
150
|
31,
|
|
151
151
|
127
|
|
152
152
|
];
|
|
153
|
+
/**
|
|
154
|
+
* A lone high and a lone low surrogate, which spec 4.8 refuses under every rule: a
|
|
155
|
+
* JavaScript string holds either, and neither is a Unicode character with a UTF-8 form.
|
|
156
|
+
*/
|
|
157
|
+
const refusedLoneSurrogates = [55296, 56320];
|
|
153
158
|
/** The byte length spec 4.8 allows a writable key, which two key lists sit on either side of. */
|
|
154
159
|
const writableKeyLimit = 1024;
|
|
155
160
|
/** The prefix one case writes below, which no other case reads or writes. */
|
|
@@ -161,7 +166,7 @@ function keyFor(ctx, caseName, name = "object") {
|
|
|
161
166
|
}
|
|
162
167
|
/**
|
|
163
168
|
* A key of exactly `bytes` UTF-8 bytes below `prefix`, in segments of at most 255 bytes:
|
|
164
|
-
* the boundary key of spec
|
|
169
|
+
* the boundary key of spec 9.2 and the over-long key of spec 9.7 are both built here, so
|
|
165
170
|
* that the run's prefix counts toward the length rather than being added to it.
|
|
166
171
|
*/
|
|
167
172
|
function keyOfBytes(prefix, bytes) {
|
|
@@ -171,7 +176,7 @@ function keyOfBytes(prefix, bytes) {
|
|
|
171
176
|
const characters = fill - (count - 1);
|
|
172
177
|
return `${prefix}${Array.from({ length: count }, (_, index) => "k".repeat(Math.floor(characters / count) + (index < characters % count ? 1 : 0))).join("/")}`;
|
|
173
178
|
}
|
|
174
|
-
/** The accepted list of spec
|
|
179
|
+
/** The accepted list of spec 9.7, below the prefix of the case that writes it. */
|
|
175
180
|
function acceptedKeys(prefix) {
|
|
176
181
|
return [
|
|
177
182
|
...acceptedNames.map((name) => ({
|
|
@@ -200,7 +205,7 @@ const acceptedNames = [
|
|
|
200
205
|
"Grüße/日本語/ключ.txt"
|
|
201
206
|
];
|
|
202
207
|
/**
|
|
203
|
-
* The list of spec
|
|
208
|
+
* The list of spec 9.7 that `put` refuses, below the prefix of the case wherever the
|
|
204
209
|
* prefix leaves the violated rule as it is: a key that is about the start of a key is
|
|
205
210
|
* given as the spec writes it, and every other one goes below the run's prefix, so that
|
|
206
211
|
* a run against a shared bucket asks `exists` about its own key space alone.
|
|
@@ -247,11 +252,14 @@ function refusedWritableKeys(prefix) {
|
|
|
247
252
|
key: `${prefix}a\\b`,
|
|
248
253
|
existsAnswers: "false"
|
|
249
254
|
},
|
|
250
|
-
...refusedControlCharacters.map((code) =>
|
|
251
|
-
label
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
...[...refusedControlCharacters, ...refusedLoneSurrogates].map((code) => {
|
|
256
|
+
const { label, key } = keyHolding(prefix, code);
|
|
257
|
+
return {
|
|
258
|
+
label,
|
|
259
|
+
key,
|
|
260
|
+
existsAnswers: "unasked"
|
|
261
|
+
};
|
|
262
|
+
}),
|
|
255
263
|
{
|
|
256
264
|
label: "a key of 1025 bytes",
|
|
257
265
|
key: keyOfBytes(prefix, 1025),
|
|
@@ -259,6 +267,46 @@ function refusedWritableKeys(prefix) {
|
|
|
259
267
|
}
|
|
260
268
|
];
|
|
261
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* The list of spec 9.7 that `get`, `stat` and `exists` refuse, placed as the refused
|
|
272
|
+
* writable list places its keys.
|
|
273
|
+
*/
|
|
274
|
+
function refusedAddressableKeys(prefix) {
|
|
275
|
+
return [
|
|
276
|
+
{
|
|
277
|
+
label: "the empty string",
|
|
278
|
+
key: ""
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
label: "/a",
|
|
282
|
+
key: "/a"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
label: "a//b",
|
|
286
|
+
key: `${prefix}a//b`
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
label: "./a",
|
|
290
|
+
key: `${prefix}./a`
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
label: "a/../b",
|
|
294
|
+
key: `${prefix}a/../b`
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
label: ".",
|
|
298
|
+
key: `${prefix}.`
|
|
299
|
+
},
|
|
300
|
+
keyHolding(prefix, 0),
|
|
301
|
+
...refusedLoneSurrogates.map((code) => keyHolding(prefix, code))
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
function keyHolding(prefix, code) {
|
|
305
|
+
return {
|
|
306
|
+
label: `a key holding U+${code.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
307
|
+
key: `${prefix}a${String.fromCharCode(code)}b`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
262
310
|
//#endregion
|
|
263
311
|
//#region src/cases/objects.ts
|
|
264
312
|
/**
|
|
@@ -274,7 +322,7 @@ const textContentType = "text/plain";
|
|
|
274
322
|
*/
|
|
275
323
|
const inFlight = 32;
|
|
276
324
|
/**
|
|
277
|
-
* What the `slow` cases of spec
|
|
325
|
+
* What the `slow` cases of spec 9.5 write: one past the thousand objects a provider
|
|
278
326
|
* answers a listing with and takes in a delete at the most, so that the case reaches
|
|
279
327
|
* past the one page and the one batch rather than filling them.
|
|
280
328
|
*/
|
|
@@ -311,6 +359,11 @@ async function assertNothingBelow(ctx, prefix) {
|
|
|
311
359
|
const left = await collectEntries(ctx.storage.list({ prefix }));
|
|
312
360
|
assert(left.length === 0, `${left.length} objects are left below ${JSON.stringify(prefix)}, the first of them ${JSON.stringify(left[0]?.key)}`);
|
|
313
361
|
}
|
|
362
|
+
/** A report of spec 4.7 over keys the provider took: the count it covered, and no failure. */
|
|
363
|
+
function assertAccepted(report, requested, what) {
|
|
364
|
+
assert(report.requested === requested, `${what} reports \`requested: ${report.requested}\` and not ${requested}`);
|
|
365
|
+
assert(report.failed.length === 0, `${what} reports the failure ${JSON.stringify(report.failed[0]?.message)}`);
|
|
366
|
+
}
|
|
314
367
|
//#endregion
|
|
315
368
|
//#region src/cases/copy.ts
|
|
316
369
|
const utf8$5 = new TextEncoder();
|
|
@@ -391,11 +444,11 @@ const copyAndMoveCases = [
|
|
|
391
444
|
const to = `${prefix}destination.txt`;
|
|
392
445
|
await ctx.storage.put(from, "the body to copy", {
|
|
393
446
|
contentType: textContentType,
|
|
394
|
-
userMetadata: {
|
|
447
|
+
userMetadata: { WrittenBy: "stowage" }
|
|
395
448
|
});
|
|
396
449
|
await ctx.storage.copy(from, to);
|
|
397
450
|
const described = await ctx.storage.stat(to);
|
|
398
|
-
assert(new Map(Object.entries(described.userMetadata).map(([name, value]) => [name.toLowerCase(), value])).get("
|
|
451
|
+
assert(new Map(Object.entries(described.userMetadata).map(([name, value]) => [name.toLowerCase(), value])).get("writtenby") === "stowage", `The destination carries the user metadata ${JSON.stringify(described.userMetadata)} and not the source's`);
|
|
399
452
|
},
|
|
400
453
|
async runWithout(ctx) {
|
|
401
454
|
const prefix = prefixFor(ctx, "copy/user-metadata");
|
|
@@ -480,18 +533,19 @@ function assertNonEmptyString(value, field) {
|
|
|
480
533
|
//#endregion
|
|
481
534
|
//#region src/cases/bytes.ts
|
|
482
535
|
const kibibyte = 1024;
|
|
483
|
-
/** The bytes a body is measured in, where a case names a size spec
|
|
536
|
+
/** The bytes a body is measured in, where a case names a size spec 9.5 or 9.6 states. */
|
|
484
537
|
const mebibyte = 1024 * kibibyte;
|
|
485
538
|
/** ADR 0016 makes one part the threshold for a stream, so 17 MiB provokes a multipart upload. */
|
|
486
539
|
const multipartSize = 17 * mebibyte;
|
|
487
540
|
/**
|
|
488
541
|
* The bytes a case writes: a pattern it rebuilds rather than holds, so that a body the
|
|
489
542
|
* provider reassembled out of order or lost a part of shows up as a mismatch, which one
|
|
490
|
-
* of the same byte repeated would not.
|
|
543
|
+
* of the same byte repeated would not. Two patterns of different `seed` differ at every
|
|
544
|
+
* byte, so an object holding parts of both matches neither.
|
|
491
545
|
*/
|
|
492
|
-
function patternOf(size) {
|
|
546
|
+
function patternOf(size, seed = 0) {
|
|
493
547
|
const bytes = new Uint8Array(size);
|
|
494
|
-
for (let index = 0; index < size; index += 1) bytes[index] = index * 7 % 251;
|
|
548
|
+
for (let index = 0; index < size; index += 1) bytes[index] = (index * 7 + seed) % 251;
|
|
495
549
|
return bytes;
|
|
496
550
|
}
|
|
497
551
|
/** The same bytes as a stream, which is the body spec 4.2 has an adapter send in parts. */
|
|
@@ -507,6 +561,44 @@ function streamOf(bytes, chunkSize, onChunk) {
|
|
|
507
561
|
onChunk?.(Math.min(sent, bytes.byteLength));
|
|
508
562
|
} });
|
|
509
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* The bodies of `put/concurrent-writers` as streams that each hold back their end until
|
|
566
|
+
* every one was read to its last byte. An adapter cannot complete an upload before its
|
|
567
|
+
* stream ends, so before either completes, each writer has read every byte and started
|
|
568
|
+
* every full part, whatever the part size, and the pacing names no adapter. Whether a
|
|
569
|
+
* started part was answered yet is the adapter's timing, which no stream can see.
|
|
570
|
+
*/
|
|
571
|
+
function streamsEndingTogether(bodies, chunkSize) {
|
|
572
|
+
const { promise: everyEndReached, resolve } = Promise.withResolvers();
|
|
573
|
+
let unended = bodies.length;
|
|
574
|
+
return bodies.map((bytes) => {
|
|
575
|
+
let sent = 0;
|
|
576
|
+
let reachedEnd = false;
|
|
577
|
+
let canceled = false;
|
|
578
|
+
const reachEnd = () => {
|
|
579
|
+
if (reachedEnd) return;
|
|
580
|
+
reachedEnd = true;
|
|
581
|
+
unended -= 1;
|
|
582
|
+
if (unended === 0) resolve();
|
|
583
|
+
};
|
|
584
|
+
return new ReadableStream({
|
|
585
|
+
async pull(controller) {
|
|
586
|
+
if (sent < bytes.byteLength) {
|
|
587
|
+
controller.enqueue(bytes.subarray(sent, Math.min(sent + chunkSize, bytes.byteLength)));
|
|
588
|
+
sent += chunkSize;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
reachEnd();
|
|
592
|
+
await everyEndReached;
|
|
593
|
+
if (!canceled) controller.close();
|
|
594
|
+
},
|
|
595
|
+
cancel() {
|
|
596
|
+
canceled = true;
|
|
597
|
+
reachEnd();
|
|
598
|
+
}
|
|
599
|
+
}, { highWaterMark: 0 });
|
|
600
|
+
});
|
|
601
|
+
}
|
|
510
602
|
async function collect(stream) {
|
|
511
603
|
const chunks = [];
|
|
512
604
|
let size = 0;
|
|
@@ -642,11 +734,6 @@ const deleteCases = [
|
|
|
642
734
|
}
|
|
643
735
|
}
|
|
644
736
|
];
|
|
645
|
-
/** A report of spec 4.7 over keys the provider took: the count it covered, and no failure. */
|
|
646
|
-
function assertAccepted(report, requested, what) {
|
|
647
|
-
assert(report.requested === requested, `${what} reports \`requested: ${report.requested}\` and not ${requested}`);
|
|
648
|
-
assert(report.failed.length === 0, `${what} reports the failure ${JSON.stringify(report.failed[0]?.message)}`);
|
|
649
|
-
}
|
|
650
737
|
//#endregion
|
|
651
738
|
//#region src/cases/errors.ts
|
|
652
739
|
/**
|
|
@@ -697,11 +784,11 @@ const errorCases = [
|
|
|
697
784
|
async run(ctx) {
|
|
698
785
|
const prefix = prefixFor(ctx, "errors/bad-credentials");
|
|
699
786
|
const storage = await storageFrom(ctx, "createStorageWithBadCredentials");
|
|
700
|
-
await expectStorageError(() => storage.get(`${prefix}object`), {
|
|
787
|
+
const refused = await expectStorageError(() => storage.get(`${prefix}object`), {
|
|
701
788
|
code: "InvalidCredentials",
|
|
702
|
-
retryable: false
|
|
703
|
-
attempts: 1
|
|
789
|
+
retryable: false
|
|
704
790
|
});
|
|
791
|
+
assert(refused.attempts === 1 || refused.attempts === 2, `Expected \`InvalidCredentials\` from \`get\`, and the error carries \`attempts: ${refused.attempts}\` rather than 1 or 2`);
|
|
705
792
|
await expectAnyStorageError(() => storage.exists(`${prefix}object`), "`exists` under a credential the provider refuses");
|
|
706
793
|
await expectAnyStorageError(() => storage.list({ prefix }).page(), "`list` under a credential the provider refuses");
|
|
707
794
|
}
|
|
@@ -787,7 +874,7 @@ function assertField(thrown, field, expected, failure) {
|
|
|
787
874
|
const held = thrown[field];
|
|
788
875
|
assert(held === expected, `The error for ${failure.what} reports \`${field}: ${JSON.stringify(held)}\` and not ${JSON.stringify(expected)}`);
|
|
789
876
|
}
|
|
790
|
-
/** The storage of a credential factory, which spec
|
|
877
|
+
/** The storage of a credential factory, which spec 9.2 has kept the case out of a run without. */
|
|
791
878
|
async function storageFrom(ctx, factory) {
|
|
792
879
|
const create = ctx.target[factory];
|
|
793
880
|
assert(create !== void 0, `The target supplies no \`${factory}\``);
|
|
@@ -820,7 +907,7 @@ const existsCases = [{
|
|
|
820
907
|
//#endregion
|
|
821
908
|
//#region src/cases/presign.ts
|
|
822
909
|
const utf8$4 = new TextEncoder();
|
|
823
|
-
/** Past the second `presign/expired-url` signs for, which is what spec
|
|
910
|
+
/** Past the second `presign/expired-url` signs for, which is what spec 9.5 waits out. */
|
|
824
911
|
const pastTheLifetime = 2e3;
|
|
825
912
|
/** The seconds spec 7.10 allows `expiresIn`, which the two refused values sit outside. */
|
|
826
913
|
const refusedLifetimes = [0, 604801];
|
|
@@ -834,7 +921,7 @@ const presignCases = [
|
|
|
834
921
|
const key = keyFor(ctx, "presign/get", "object.txt");
|
|
835
922
|
const body = "the body a signed `GET` hands out";
|
|
836
923
|
await ctx.storage.put(key, body, { contentType: textContentType });
|
|
837
|
-
const response = await fetch(await
|
|
924
|
+
const response = await fetch(await presignedGet(ctx, key, { expiresIn: 300 }));
|
|
838
925
|
assertStatus(response.status, 200, "`fetch` on a signed `GET`");
|
|
839
926
|
assertHeader(response, "content-type", textContentType);
|
|
840
927
|
assertSameBytes(new Uint8Array(await response.arrayBuffer()), utf8$4.encode(body), "the body a signed `GET` answered with");
|
|
@@ -848,7 +935,7 @@ const presignCases = [
|
|
|
848
935
|
async run(ctx) {
|
|
849
936
|
const key = keyFor(ctx, "presign/put", "object.txt");
|
|
850
937
|
const body = utf8$4.encode("the body a signed `PUT` takes");
|
|
851
|
-
const url = await
|
|
938
|
+
const { url, headers } = await presignedPut(ctx, key, {
|
|
852
939
|
expiresIn: 300,
|
|
853
940
|
contentType: textContentType,
|
|
854
941
|
contentLength: body.byteLength
|
|
@@ -856,7 +943,7 @@ const presignCases = [
|
|
|
856
943
|
const response = await fetch(url, {
|
|
857
944
|
method: "PUT",
|
|
858
945
|
body,
|
|
859
|
-
headers
|
|
946
|
+
headers
|
|
860
947
|
});
|
|
861
948
|
assert(response.ok, `\`fetch\` on a signed \`PUT\` answered ${response.status} and not a success`);
|
|
862
949
|
await response.arrayBuffer();
|
|
@@ -884,15 +971,17 @@ const presignCases = [
|
|
|
884
971
|
async run(ctx) {
|
|
885
972
|
const key = keyFor(ctx, "presign/put-rejects-type", "object.txt");
|
|
886
973
|
const body = utf8$4.encode("a body of another type than the signature binds");
|
|
887
|
-
const
|
|
974
|
+
const presigned = await presignedPut(ctx, key, {
|
|
888
975
|
expiresIn: 300,
|
|
889
976
|
contentType: textContentType,
|
|
890
977
|
contentLength: body.byteLength
|
|
891
978
|
});
|
|
892
|
-
|
|
979
|
+
const headers = new Headers(presigned.headers);
|
|
980
|
+
headers.set("content-type", "application/json");
|
|
981
|
+
assertStatus(await statusOf(await fetch(presigned.url, {
|
|
893
982
|
method: "PUT",
|
|
894
983
|
body,
|
|
895
|
-
headers
|
|
984
|
+
headers
|
|
896
985
|
})), 403, "a signed `PUT` carrying another content type");
|
|
897
986
|
},
|
|
898
987
|
runWithout: assertNeitherMethod
|
|
@@ -904,7 +993,7 @@ const presignCases = [
|
|
|
904
993
|
async run(ctx) {
|
|
905
994
|
const key = keyFor(ctx, "presign/put-rejects-length", "object.txt");
|
|
906
995
|
const body = utf8$4.encode("a body longer than the signature binds");
|
|
907
|
-
const url = await
|
|
996
|
+
const { url, headers } = await presignedPut(ctx, key, {
|
|
908
997
|
expiresIn: 300,
|
|
909
998
|
contentType: textContentType,
|
|
910
999
|
contentLength: body.byteLength - 1
|
|
@@ -912,7 +1001,7 @@ const presignCases = [
|
|
|
912
1001
|
const status = await statusOf(await fetch(url, {
|
|
913
1002
|
method: "PUT",
|
|
914
1003
|
body,
|
|
915
|
-
headers
|
|
1004
|
+
headers
|
|
916
1005
|
}));
|
|
917
1006
|
assert(status >= 400 && status < 500, `A signed \`PUT\` carrying another length was answered ${status} and not refused`);
|
|
918
1007
|
},
|
|
@@ -925,7 +1014,7 @@ const presignCases = [
|
|
|
925
1014
|
async run(ctx) {
|
|
926
1015
|
const key = keyFor(ctx, "presign/expired-url", "object.txt");
|
|
927
1016
|
await ctx.storage.put(key, "the body the URL stops handing out", { contentType: textContentType });
|
|
928
|
-
const url = await
|
|
1017
|
+
const url = await presignedGet(ctx, key, { expiresIn: 1 });
|
|
929
1018
|
await delay(pastTheLifetime);
|
|
930
1019
|
assertStatus(await statusOf(await fetch(url)), 403, "a signed `GET` that has expired");
|
|
931
1020
|
},
|
|
@@ -940,12 +1029,41 @@ const presignCases = [
|
|
|
940
1029
|
async function assertNeitherMethod(ctx) {
|
|
941
1030
|
for (const name of presignNames) assert(!(name in ctx.storage), `The storage declares no \`presignedUrls\` and carries \`${name}\``);
|
|
942
1031
|
}
|
|
943
|
-
/** The URL
|
|
944
|
-
async function
|
|
945
|
-
const url = await presignerOf(ctx.storage,
|
|
946
|
-
assert(
|
|
1032
|
+
/** The URL `presignGet` handed back, which a target may report as anything at runtime. */
|
|
1033
|
+
async function presignedGet(ctx, key, options) {
|
|
1034
|
+
const url = await presignerOf(ctx.storage, "presignGet")(key, options);
|
|
1035
|
+
assert(isUrl(url), `\`presignGet\` handed back ${JSON.stringify(url)} and not a URL`);
|
|
947
1036
|
return url;
|
|
948
1037
|
}
|
|
1038
|
+
/**
|
|
1039
|
+
* What `presignPut` handed back, which a target may report as anything at runtime. The
|
|
1040
|
+
* upload sends its headers as they are, so the suite names no provider (spec 4.13).
|
|
1041
|
+
*/
|
|
1042
|
+
async function presignedPut(ctx, key, options) {
|
|
1043
|
+
const presigned = await presignerOf(ctx.storage, "presignPut")(key, options);
|
|
1044
|
+
assert(isPresignedPut(presigned), `\`presignPut\` handed back ${JSON.stringify(presigned)} and not a URL with the headers that go beside the body`);
|
|
1045
|
+
return presigned;
|
|
1046
|
+
}
|
|
1047
|
+
function isUrl(value) {
|
|
1048
|
+
return typeof value === "string" && value !== "";
|
|
1049
|
+
}
|
|
1050
|
+
/** The shape spec 4.13 gives `PresignedPut`, its rule on `Content-Length` included. */
|
|
1051
|
+
function isPresignedPut(value) {
|
|
1052
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1053
|
+
const url = Reflect.get(value, "url");
|
|
1054
|
+
const headers = Reflect.get(value, "headers");
|
|
1055
|
+
if (!isUrl(url) || !isPlainObject(headers)) return false;
|
|
1056
|
+
return Object.entries(headers).every(([name, field]) => typeof field === "string" && name.toLowerCase() !== "content-length");
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* An array or a `Headers` would pass the entries check above: the one lists its indices,
|
|
1060
|
+
* the other lists nothing at all.
|
|
1061
|
+
*/
|
|
1062
|
+
function isPlainObject(value) {
|
|
1063
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1064
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1065
|
+
return prototype === Object.prototype || prototype === null;
|
|
1066
|
+
}
|
|
949
1067
|
function presignerOf(storage, name) {
|
|
950
1068
|
const held = Reflect.get(storage, name);
|
|
951
1069
|
assert(typeof held === "function", `The storage declares \`presignedUrls\` and carries no \`${name}\``);
|
|
@@ -1023,7 +1141,7 @@ const flowCases = [
|
|
|
1023
1141
|
async run(ctx) {
|
|
1024
1142
|
const key = keyFor(ctx, "flow/2-presigned-put", "upload.txt");
|
|
1025
1143
|
const body = utf8$3.encode("the body a browser uploads to the provider");
|
|
1026
|
-
const url = await
|
|
1144
|
+
const { url, headers } = await presignedPut(ctx, key, {
|
|
1027
1145
|
expiresIn: 300,
|
|
1028
1146
|
contentType: textContentType,
|
|
1029
1147
|
contentLength: body.byteLength
|
|
@@ -1031,7 +1149,7 @@ const flowCases = [
|
|
|
1031
1149
|
const response = await fetch(url, {
|
|
1032
1150
|
method: "PUT",
|
|
1033
1151
|
body,
|
|
1034
|
-
headers
|
|
1152
|
+
headers
|
|
1035
1153
|
});
|
|
1036
1154
|
assert(response.ok, `The upload to the presigned URL was answered ${response.status}`);
|
|
1037
1155
|
await response.arrayBuffer();
|
|
@@ -1238,6 +1356,23 @@ const getCases = [
|
|
|
1238
1356
|
await Promise.all(absent.map(async ({ label, key }) => await expectStorageError(() => ctx.storage.get(key), { code: "NotFound" }, label)));
|
|
1239
1357
|
}
|
|
1240
1358
|
},
|
|
1359
|
+
{
|
|
1360
|
+
name: "get/refused-keys",
|
|
1361
|
+
requires: [],
|
|
1362
|
+
cost: "fast",
|
|
1363
|
+
async run(ctx) {
|
|
1364
|
+
const refused = refusedAddressableKeys(prefixFor(ctx, "get/refused-keys"));
|
|
1365
|
+
const reads = [
|
|
1366
|
+
["get", (key) => ctx.storage.get(key)],
|
|
1367
|
+
["stat", (key) => ctx.storage.stat(key)],
|
|
1368
|
+
["exists", (key) => ctx.storage.exists(key)]
|
|
1369
|
+
];
|
|
1370
|
+
await Promise.all(refused.flatMap(({ label, key }) => reads.map(async ([operation, read]) => await expectStorageError(() => read(key), {
|
|
1371
|
+
code: "InvalidKey",
|
|
1372
|
+
attempts: 0
|
|
1373
|
+
}, `\`${operation}\` of ${label}`))));
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1241
1376
|
{
|
|
1242
1377
|
name: "get/aborted-signal",
|
|
1243
1378
|
requires: [],
|
|
@@ -1539,6 +1674,23 @@ const listCases = [
|
|
|
1539
1674
|
assert(listed?.normalize() === key.normalize(), `The listing names the ${form} key as ${JSON.stringify(listed)}, which is no Unicode equivalent of ${JSON.stringify(key)}`);
|
|
1540
1675
|
}));
|
|
1541
1676
|
}
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
name: "list/noncharacter-key",
|
|
1680
|
+
requires: [],
|
|
1681
|
+
cost: "fast",
|
|
1682
|
+
async run(ctx) {
|
|
1683
|
+
const prefix = prefixFor(ctx, "list/noncharacter-key");
|
|
1684
|
+
const key = `${prefix}noncharacter-${String.fromCodePoint(65534)}.txt`;
|
|
1685
|
+
const body = patternOf(16);
|
|
1686
|
+
await ctx.storage.put(key, body);
|
|
1687
|
+
assertNamesEachOnce((await collectEntries(ctx.storage.list({ prefix }))).map((entry) => entry.key), [key], "The iteration below the prefix");
|
|
1688
|
+
const stored = await ctx.storage.get(key);
|
|
1689
|
+
assert(stored.stat.key === key, `\`get\` describes the object as ${JSON.stringify(stored.stat.key)} and not as the key it was asked for`);
|
|
1690
|
+
assertSameBytes(await stored.bytes(), body, "the object under the key holding U+FFFE");
|
|
1691
|
+
assertAccepted(await ctx.storage.delete(key), 1, "Deleting the key holding U+FFFE");
|
|
1692
|
+
await assertNothingBelow(ctx, prefix);
|
|
1693
|
+
}
|
|
1542
1694
|
}
|
|
1543
1695
|
];
|
|
1544
1696
|
/**
|
|
@@ -1561,6 +1713,10 @@ function assertNamesTheOption(message, option) {
|
|
|
1561
1713
|
const utf8$1 = new TextEncoder();
|
|
1562
1714
|
/** Spec 4.3 takes an ASCII HTTP token as a user metadata key, which this is not. */
|
|
1563
1715
|
const metadataKeyAboveAscii = { grüße: "hallo" };
|
|
1716
|
+
/** No Unicode character, so no UTF-8 form for the 2 KB of spec 4.3 to measure. */
|
|
1717
|
+
const metadataValueWithLoneSurrogate = { note: "lone-\ud800" };
|
|
1718
|
+
/** An ASCII HTTP token and no identifier, which spec 4.9 promises under `userMetadataTokenKeys`. */
|
|
1719
|
+
const metadataTokenKey = { "content-hash": "sha256-conformance" };
|
|
1564
1720
|
/** Over the 2 KB of encoded header bytes spec 4.3 allows a whole user metadata set. */
|
|
1565
1721
|
const metadataOverTheLimit = { note: "x".repeat(2 * kibibyte) };
|
|
1566
1722
|
const putCases = [
|
|
@@ -1761,7 +1917,7 @@ const putCases = [
|
|
|
1761
1917
|
async run(ctx) {
|
|
1762
1918
|
const key = keyFor(ctx, "put/user-metadata");
|
|
1763
1919
|
const userMetadata = {
|
|
1764
|
-
|
|
1920
|
+
WrittenBy: "stowage",
|
|
1765
1921
|
run: "conformance"
|
|
1766
1922
|
};
|
|
1767
1923
|
await ctx.storage.put(key, patternOf(16), { userMetadata });
|
|
@@ -1771,7 +1927,7 @@ const putCases = [
|
|
|
1771
1927
|
async runWithout(ctx) {
|
|
1772
1928
|
const refused = keyFor(ctx, "put/user-metadata", "refused");
|
|
1773
1929
|
const empty = keyFor(ctx, "put/user-metadata", "empty");
|
|
1774
|
-
await expectUnsupported(() => ctx.storage.put(refused, patternOf(16), { userMetadata: {
|
|
1930
|
+
await expectUnsupported(() => ctx.storage.put(refused, patternOf(16), { userMetadata: { WrittenBy: "stowage" } }), "userMetadata");
|
|
1775
1931
|
await ctx.storage.put(empty, patternOf(16), { userMetadata: {} });
|
|
1776
1932
|
assertNoMetadata((await ctx.storage.stat(empty)).userMetadata, "`stat`");
|
|
1777
1933
|
assertNoMetadata((await ctx.storage.get(empty)).stat.userMetadata, "`get`");
|
|
@@ -1788,6 +1944,10 @@ const putCases = [
|
|
|
1788
1944
|
code: "InvalidRequest",
|
|
1789
1945
|
attempts: 0
|
|
1790
1946
|
}, "a user metadata key above ASCII");
|
|
1947
|
+
await expectStorageError(() => ctx.storage.put(key, bytes, { userMetadata: metadataValueWithLoneSurrogate }), {
|
|
1948
|
+
code: "InvalidRequest",
|
|
1949
|
+
attempts: 0
|
|
1950
|
+
}, "a user metadata value holding a lone surrogate");
|
|
1791
1951
|
await expectStorageError(() => ctx.storage.put(key, bytes, { userMetadata: metadataOverTheLimit }), {
|
|
1792
1952
|
code: "InvalidRequest",
|
|
1793
1953
|
attempts: 0
|
|
@@ -1797,10 +1957,50 @@ const putCases = [
|
|
|
1797
1957
|
const key = keyFor(ctx, "put/user-metadata-limits");
|
|
1798
1958
|
const bytes = patternOf(16);
|
|
1799
1959
|
await expectUnsupported(() => ctx.storage.put(key, bytes, { userMetadata: metadataKeyAboveAscii }), "userMetadata");
|
|
1960
|
+
await expectUnsupported(() => ctx.storage.put(key, bytes, { userMetadata: metadataValueWithLoneSurrogate }), "userMetadata");
|
|
1800
1961
|
await expectUnsupported(() => ctx.storage.put(key, bytes, { userMetadata: metadataOverTheLimit }), "userMetadata");
|
|
1801
1962
|
}
|
|
1963
|
+
},
|
|
1964
|
+
{
|
|
1965
|
+
name: "put/user-metadata-token-keys",
|
|
1966
|
+
requires: ["userMetadata", "userMetadataTokenKeys"],
|
|
1967
|
+
cost: "fast",
|
|
1968
|
+
async run(ctx) {
|
|
1969
|
+
const key = keyFor(ctx, "put/user-metadata-token-keys");
|
|
1970
|
+
await ctx.storage.put(key, patternOf(16), { userMetadata: metadataTokenKey });
|
|
1971
|
+
assertHoldsMetadata((await ctx.storage.stat(key)).userMetadata, metadataTokenKey, "`stat`");
|
|
1972
|
+
assertHoldsMetadata((await ctx.storage.get(key)).stat.userMetadata, metadataTokenKey, "`get`");
|
|
1973
|
+
},
|
|
1974
|
+
async runWithout(ctx) {
|
|
1975
|
+
const key = keyFor(ctx, "put/user-metadata-token-keys");
|
|
1976
|
+
await expectStorageError(() => ctx.storage.put(key, patternOf(16), { userMetadata: metadataTokenKey }), {
|
|
1977
|
+
code: "Unsupported",
|
|
1978
|
+
attempts: 0,
|
|
1979
|
+
capability: ctx.declares("userMetadata") ? "userMetadataTokenKeys" : "userMetadata"
|
|
1980
|
+
}, "a user metadata key beyond identifiers");
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
name: "put/concurrent-writers",
|
|
1985
|
+
requires: [],
|
|
1986
|
+
cost: "fast",
|
|
1987
|
+
async run(ctx) {
|
|
1988
|
+
const key = keyFor(ctx, "put/concurrent-writers");
|
|
1989
|
+
const patterns = [patternOf(multipartSize), patternOf(multipartSize, 1)];
|
|
1990
|
+
const bodies = streamsEndingTogether(patterns, mebibyte);
|
|
1991
|
+
const outcomes = await Promise.allSettled(bodies.map((body) => ctx.storage.put(key, body)));
|
|
1992
|
+
assert(outcomes.some((outcome) => outcome.status === "fulfilled"), `Both writers rejected: ${outcomes.map(describeOutcome).join("; ")}`);
|
|
1993
|
+
const held = await collect((await ctx.storage.get(key)).stream());
|
|
1994
|
+
assert(patterns.some((pattern) => sameBytes(held, pattern)), `The key holds ${held.byteLength} bytes that are neither writer's object whole`);
|
|
1995
|
+
}
|
|
1802
1996
|
}
|
|
1803
1997
|
];
|
|
1998
|
+
function describeOutcome(outcome) {
|
|
1999
|
+
return outcome.status === "fulfilled" ? "resolved" : String(outcome.reason);
|
|
2000
|
+
}
|
|
2001
|
+
function sameBytes(one, other) {
|
|
2002
|
+
return one.byteLength === other.byteLength && one.every((byte, index) => byte === other[index]);
|
|
2003
|
+
}
|
|
1804
2004
|
function assertContentType(described, expected, where) {
|
|
1805
2005
|
assert(described.contentType === expected, `${where} reports the content type ${JSON.stringify(described.contentType)} and not ${JSON.stringify(expected)}`);
|
|
1806
2006
|
}
|
|
@@ -1878,11 +2078,11 @@ const conformanceCaseSources = [
|
|
|
1878
2078
|
...presignCases,
|
|
1879
2079
|
...flowCases
|
|
1880
2080
|
];
|
|
1881
|
-
/** The same cases as the package publishes them, without the factory of spec
|
|
2081
|
+
/** The same cases as the package publishes them, without the factory of spec 9.3. */
|
|
1882
2082
|
const conformanceCases = conformanceCaseSources;
|
|
1883
2083
|
//#endregion
|
|
1884
2084
|
//#region src/run.ts
|
|
1885
|
-
/** The cases a run performs: the `fast` tier alone unless it asks for both (spec
|
|
2085
|
+
/** The cases a run performs: the `fast` tier alone unless it asks for both (spec 9.2). */
|
|
1886
2086
|
function selectedCases(options) {
|
|
1887
2087
|
if (options?.includeSlow === true) return conformanceCaseSources;
|
|
1888
2088
|
return conformanceCaseSources.filter((source) => source.cost === "fast");
|
|
@@ -1892,7 +2092,7 @@ function createKeyPrefix() {
|
|
|
1892
2092
|
return `${keyPrefixRoot}/${crypto.randomUUID()}/`;
|
|
1893
2093
|
}
|
|
1894
2094
|
/**
|
|
1895
|
-
* Spec
|
|
2095
|
+
* Spec 9.2: the declaration is read once per run, before the first case, because every
|
|
1896
2096
|
* storage the target creates in one run declares the same.
|
|
1897
2097
|
*/
|
|
1898
2098
|
async function startRun(target, keyPrefix) {
|
|
@@ -1906,7 +2106,7 @@ async function startRun(target, keyPrefix) {
|
|
|
1906
2106
|
};
|
|
1907
2107
|
}
|
|
1908
2108
|
/**
|
|
1909
|
-
* Spec
|
|
2109
|
+
* Spec 9.2: the default deletes below the prefix on a storage of its own, so that a run
|
|
1910
2110
|
* whose storage broke on the way still clears what it wrote.
|
|
1911
2111
|
*/
|
|
1912
2112
|
async function cleanUp(target, keyPrefix) {
|
|
@@ -1914,13 +2114,13 @@ async function cleanUp(target, keyPrefix) {
|
|
|
1914
2114
|
const [failure] = (await (await target.createStorage()).deleteAll(keyPrefix)).failed;
|
|
1915
2115
|
if (failure !== void 0) throw failure;
|
|
1916
2116
|
}
|
|
1917
|
-
/** The factory the case needs and the target left out, which spec
|
|
2117
|
+
/** The factory the case needs and the target left out, which spec 9.2 skips it for. */
|
|
1918
2118
|
function skipReasonFor(source, target) {
|
|
1919
2119
|
if (source.factory === void 0 || target[source.factory] !== void 0) return void 0;
|
|
1920
2120
|
return source.factory;
|
|
1921
2121
|
}
|
|
1922
2122
|
/**
|
|
1923
|
-
* Spec
|
|
2123
|
+
* Spec 9.2: a case whose requirements are all declared runs `run`, and one missing a
|
|
1924
2124
|
* name runs `runWithout`.
|
|
1925
2125
|
*/
|
|
1926
2126
|
function selectHalf(source, context) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stowage/conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The cases every stowage adapter has to pass, as plain cases a test framework maps onto its own runner.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adapter",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"provenance": true
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@stowage/core": "^0.
|
|
36
|
+
"@stowage/core": "^0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=24"
|