@stowage/conformance 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Kaufmann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @stowage/conformance
2
+
3
+ The cases every stowage adapter has to pass, as plain cases a test framework maps onto its own
4
+ runner. An adapter written outside this repository passes the same cases as the ones inside it.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install --save-dev @stowage/conformance
10
+ ```
11
+
12
+ ## Write a target
13
+
14
+ A `ConformanceTarget` tells the suite how to construct the storage under test. `createStorage`
15
+ returns a storage the run may write to below any prefix, constructed from outside the adapter the
16
+ way a caller would construct it
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
+
19
+ ```ts
20
+ import { memoryStorage } from "@stowage/adapter-memory";
21
+ import type { ConformanceTarget } from "@stowage/conformance";
22
+
23
+ export const target: ConformanceTarget = {
24
+ name: "@stowage/adapter-memory",
25
+ createStorage: () => memoryStorage(),
26
+ };
27
+ ```
28
+
29
+ The optional members widen the run:
30
+
31
+ - `cleanup(keyPrefix)` runs once at the end, and defaults to `deleteAll(keyPrefix)` on a fresh
32
+ storage. Every key a run writes begins with its prefix, so two runs against one bucket do not
33
+ interfere.
34
+ - `createStorageWithBadCredentials`, `createStorageWithExpiredCredentials` and
35
+ `createStorageWithDeniedCredentials` return storages whose credential the provider refuses, has
36
+ expired, or accepts for reading alone. A case that needs a factory the target leaves out is
37
+ reported `skipped`.
38
+
39
+ ## Fill the declaration
40
+
41
+ The suite reads what the storage supports from its `capabilities`, once per run, before the first
42
+ case. The storage lists every name out of `capabilityNames` it implements, each once, and the list
43
+ is fixed when it is constructed
44
+ ([spec 4.9](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#49-capabilities)).
45
+ Every storage the target creates in one run declares the same; two configurations are two targets.
46
+
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)).
53
+ An adapter refuses such a call like this:
54
+
55
+ ```ts
56
+ import { StorageError } from "@stowage/core";
57
+
58
+ export function refuseUserMetadata(bucket: string): StorageError {
59
+ return new StorageError({
60
+ code: "Unsupported",
61
+ message: "This storage keeps no user metadata",
62
+ operation: "put",
63
+ bucket,
64
+ provider: "my-provider",
65
+ attempts: 0,
66
+ capability: "userMetadata",
67
+ });
68
+ }
69
+ ```
70
+
71
+ `expectUnsupported(call, capability)` asserts that refusal in a test of the adapter's own.
72
+
73
+ ## Run the cases
74
+
75
+ `describeConformance(target, framework)` maps every case onto the `describe` and `test` of the
76
+ framework it is handed
77
+ ([spec 9.2](https://github.com/stowage-js/stowage/blob/@stowage/conformance@0.2.0/docs/spec.md#92-running-the-suite)).
78
+ It runs the `fast` cases, and both tiers with `includeSlow: true`.
79
+
80
+ Vitest:
81
+
82
+ ```ts
83
+ import { memoryStorage } from "@stowage/adapter-memory";
84
+ import { describeConformance } from "@stowage/conformance";
85
+ import { describe, test } from "vitest";
86
+
87
+ describeConformance(
88
+ { name: "@stowage/adapter-memory", createStorage: () => memoryStorage() },
89
+ { describe, test, includeSlow: true },
90
+ );
91
+ ```
92
+
93
+ `bun:test`:
94
+
95
+ ```ts
96
+ import { describe, test } from "bun:test";
97
+
98
+ import { memoryStorage } from "@stowage/adapter-memory";
99
+ import { describeConformance } from "@stowage/conformance";
100
+
101
+ describeConformance(
102
+ { name: "@stowage/adapter-memory", createStorage: () => memoryStorage() },
103
+ { describe, test },
104
+ );
105
+ ```
106
+
107
+ `Deno.test` has no `describe` beside it, so a block becomes the leading part of each test's name:
108
+
109
+ ```ts
110
+ import { memoryStorage } from "@stowage/adapter-memory";
111
+ import { describeConformance } from "@stowage/conformance";
112
+
113
+ const enclosingNames: string[] = [];
114
+
115
+ describeConformance(
116
+ { name: "@stowage/adapter-memory", createStorage: () => memoryStorage() },
117
+ {
118
+ describe(name, body) {
119
+ enclosingNames.push(name);
120
+
121
+ try {
122
+ body();
123
+ } finally {
124
+ enclosingNames.pop();
125
+ }
126
+ },
127
+ test(name, body) {
128
+ Deno.test([...enclosingNames, name].join(" > "), body);
129
+ },
130
+ },
131
+ );
132
+ ```
133
+
134
+ ## Run the cases on `workerd`
135
+
136
+ `workerd` has no test framework. `runAll(target)` runs every case and returns the results, which a
137
+ worker answers with for the process that started it to report:
138
+
139
+ ```ts
140
+ import { memoryStorage } from "@stowage/adapter-memory";
141
+ import { runAll } from "@stowage/conformance";
142
+
143
+ export default {
144
+ async fetch(): Promise<Response> {
145
+ const results = await runAll({
146
+ name: "@stowage/adapter-memory",
147
+ createStorage: () => memoryStorage(),
148
+ });
149
+ const failed = results.filter((result) => result.status === "failed");
150
+
151
+ return Response.json(results, { status: failed.length === 0 ? 200 : 500 });
152
+ },
153
+ };
154
+ ```
155
+
156
+ A failed result carries the error as `name`, `message`, `stack` and, for a `StorageError`, `code`.
157
+
158
+ ## Read an adapter
159
+
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
165
+ written against the behavior of S3, not against it.
166
+
167
+ What the suite leaves to an adapter's own tests, such as flat memory during a large upload or the
168
+ retry curve, is listed in
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).
170
+
171
+ ## Runtimes
172
+
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.
174
+
175
+ The bundle measures 11.6 kB minified and gzipped, `@stowage/core` included.
176
+
177
+ ## Specification
178
+
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)
180
+ is the contract: a caller may rely on what it states and on nothing else this package happens to
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)
183
+ are at the same tag.
184
+
185
+ ## License
186
+
187
+ MIT
@@ -0,0 +1,97 @@
1
+ import { CapabilityName, Storage, StorageErrorCode } from "@stowage/core";
2
+ //#region src/assertions.d.ts
3
+ /**
4
+ * Spec 4.9 leaves a call needing a capability the storage does not declare with an
5
+ * `Unsupported` error naming it, and spec 9.2 has most of the `runWithout` halves assert
6
+ * that through this.
7
+ */
8
+ export declare function expectUnsupported(call: () => Promise<unknown>, capability: CapabilityName): Promise<void>;
9
+ //#endregion
10
+ //#region src/target.d.ts
11
+ interface ConformanceTarget {
12
+ readonly name: string;
13
+ createStorage(): Storage | Promise<Storage>;
14
+ cleanup?(keyPrefix: string): Promise<void>;
15
+ createStorageWithBadCredentials?(): Storage | Promise<Storage>;
16
+ createStorageWithExpiredCredentials?(): Storage | Promise<Storage>;
17
+ createStorageWithDeniedCredentials?(): Storage | Promise<Storage>;
18
+ }
19
+ interface ConformanceContext {
20
+ readonly storage: Storage;
21
+ readonly keyPrefix: string;
22
+ readonly target: ConformanceTarget;
23
+ declares(name: CapabilityName): boolean;
24
+ }
25
+ //#endregion
26
+ //#region src/case.d.ts
27
+ type ConformanceCase = {
28
+ readonly name: string;
29
+ readonly requires: readonly [];
30
+ readonly cost: "fast" | "slow";
31
+ run(ctx: ConformanceContext): Promise<void>;
32
+ } | {
33
+ readonly name: string;
34
+ readonly requires: readonly [CapabilityName, ...CapabilityName[]];
35
+ readonly cost: "fast" | "slow";
36
+ run(ctx: ConformanceContext): Promise<void>;
37
+ runWithout(ctx: ConformanceContext): Promise<void>;
38
+ };
39
+ interface ConformanceCaseMetadata {
40
+ readonly name: string;
41
+ readonly requires: readonly CapabilityName[];
42
+ readonly cost: "fast" | "slow";
43
+ }
44
+ //#endregion
45
+ //#region src/cases/index.d.ts
46
+ /** The same cases as the package publishes them, without the factory of spec 9.3. */
47
+ export declare const conformanceCases: readonly ConformanceCase[];
48
+ //#endregion
49
+ //#region src/result.d.ts
50
+ type ConformanceMode = "declared" | "without";
51
+ interface SerializedConformanceError {
52
+ readonly name: string;
53
+ readonly message: string;
54
+ readonly stack?: string;
55
+ readonly code?: StorageErrorCode;
56
+ }
57
+ type ConformanceResult = {
58
+ readonly case: ConformanceCaseMetadata;
59
+ readonly status: "passed";
60
+ readonly mode: ConformanceMode;
61
+ } | {
62
+ readonly case: ConformanceCaseMetadata;
63
+ readonly status: "skipped";
64
+ readonly reason: string;
65
+ } | {
66
+ readonly case: ConformanceCaseMetadata;
67
+ readonly status: "failed";
68
+ readonly mode: ConformanceMode;
69
+ readonly error: SerializedConformanceError;
70
+ };
71
+ //#endregion
72
+ //#region src/run.d.ts
73
+ interface ConformanceRunOptions {
74
+ includeSlow?: boolean;
75
+ }
76
+ //#endregion
77
+ //#region src/describe.d.ts
78
+ interface ConformanceFramework extends ConformanceRunOptions {
79
+ describe(name: string, body: () => void): void;
80
+ test(name: string, body: () => Promise<void>): void;
81
+ }
82
+ /**
83
+ * Maps every case onto one test of Vitest, `bun:test` or `Deno.test`, whose `describe`
84
+ * and `test` agree in shape (ADR 0006), so that a failing case is a failing test of its
85
+ * own rather than one red line covering the suite.
86
+ */
87
+ export declare function describeConformance(target: ConformanceTarget, framework: ConformanceFramework): void;
88
+ //#endregion
89
+ //#region src/run-all.d.ts
90
+ /**
91
+ * Runs the cases without a test framework and reports them, which is how a runtime that
92
+ * has none — `workerd` — runs the suite (ADR 0006). A failing case is one result among
93
+ * the others; only a target that cannot produce a storage at all rejects the run.
94
+ */
95
+ export declare function runAll(target: ConformanceTarget, options?: ConformanceRunOptions): Promise<readonly ConformanceResult[]>;
96
+ //#endregion
97
+ export type { ConformanceCase, ConformanceCaseMetadata, ConformanceContext, ConformanceFramework, ConformanceMode, ConformanceResult, ConformanceRunOptions, ConformanceTarget, SerializedConformanceError };