@hot-updater/test-utils 1.0.0-rc.17 → 1.0.0-rc.22
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 +97 -14
- package/dist/index.d.mts +182 -145
- package/dist/index.mjs +1697 -60
- package/dist/node.cjs +210 -0
- package/dist/node.d.cts +49 -4
- package/dist/node.d.mts +49 -4
- package/dist/node.mjs +203 -1
- package/dist/react-native.cjs +177 -0
- package/dist/react-native.d.cts +104 -0
- package/dist/react-native.d.mts +104 -0
- package/dist/react-native.mjs +174 -0
- package/package.json +44 -6
package/README.md
CHANGED
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
# @hot-updater/test-utils
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Test suites and helpers for Hot Updater storage adapters, database adapters,
|
|
4
|
+
providers, server plugins, client plugins and servers. The official adapters,
|
|
5
|
+
providers, plugins and example servers run these same suites.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
8
|
pnpm add -D @hot-updater/test-utils @hot-updater/server vitest
|
|
8
9
|
```
|
|
9
10
|
|
|
11
|
+
It uses the other Hot Updater packages only through their public API, and
|
|
12
|
+
takes them as peer dependencies. `@hot-updater/server` and
|
|
13
|
+
`@hot-updater/plugin-insights` are optional: the database suites load the
|
|
14
|
+
server when they run, and the Insights suites use the plugin's types. The SQL
|
|
15
|
+
test executors of `@hot-updater/test-utils/node` take `pg`, `mysql2` or
|
|
16
|
+
`@electric-sql/pglite`, whichever you test on.
|
|
17
|
+
|
|
10
18
|
## Database adapters and providers
|
|
11
19
|
|
|
12
20
|
Register three suites against a disposable test database:
|
|
13
21
|
|
|
14
|
-
- `setupDatabaseAdapterConformanceSuite` runs the
|
|
22
|
+
- `setupDatabaseAdapterConformanceSuite` runs the database adapter contract case
|
|
15
23
|
by case: value round-trips, ordering, guards, all-or-nothing writes, unique
|
|
16
24
|
and multi-valued indexes, paging under capped native pages, and concurrent
|
|
17
25
|
writers.
|
|
18
26
|
- `setupReadBudgetTestSuite` checks what each read of core and the built-in
|
|
19
|
-
plugins costs at the adapter,
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
plugins costs at the adapter, on `createMeasuredDatabase`: `createHotUpdater`
|
|
28
|
+
over `meterReads({ name, adapter: verifyAdapter(adapter) })` from
|
|
29
|
+
`@hot-updater/plugin-core`. `postgresRowsExamined` and `mysqlRowsExamined`
|
|
30
|
+
also count the rows a SQL database examined.
|
|
22
31
|
- `setupDatabaseTestSuite` runs core, Bundles and the Release Catalog contract
|
|
23
32
|
through the admin and client HTTP APIs of `createHotUpdater`, then the suites
|
|
24
33
|
of the server plugins listed in `plugins`, such as the Insights plugin's
|
|
25
|
-
`insightsTestSuite()
|
|
34
|
+
`insightsTestSuite()`.
|
|
26
35
|
|
|
27
36
|
Connect your database's lifecycle and the real server:
|
|
28
37
|
|
|
29
38
|
```ts
|
|
30
39
|
import { createHotUpdater } from "@hot-updater/server";
|
|
31
|
-
import { createDatabasePluginApis } from "@hot-updater/server/db";
|
|
32
40
|
import {
|
|
33
41
|
createInsightsModel,
|
|
34
42
|
insights,
|
|
35
43
|
} from "@hot-updater/server/plugins/insights";
|
|
36
|
-
|
|
44
|
+
|
|
37
45
|
import {
|
|
46
|
+
insightsTestSuite,
|
|
38
47
|
setupDatabaseTestSuite,
|
|
39
48
|
startHttpTestServer,
|
|
40
49
|
} from "@hot-updater/test-utils";
|
|
@@ -63,9 +72,14 @@ setupDatabaseTestSuite({
|
|
|
63
72
|
// The server above runs insights(), so its suite runs too.
|
|
64
73
|
plugins: [
|
|
65
74
|
insightsTestSuite({
|
|
75
|
+
// The plugin's API on the database, as a server running it assembles it.
|
|
66
76
|
createModel: (database) =>
|
|
67
77
|
createInsightsModel(
|
|
68
|
-
|
|
78
|
+
createHotUpdater({
|
|
79
|
+
database,
|
|
80
|
+
plugins: [insights()],
|
|
81
|
+
clientAccess: "public",
|
|
82
|
+
}).api.insights,
|
|
69
83
|
),
|
|
70
84
|
}),
|
|
71
85
|
],
|
|
@@ -80,15 +94,84 @@ client selector and keep a simulated device's state across updates, rollbacks
|
|
|
80
94
|
and the built-in fallback. Native download, activation and restart remain
|
|
81
95
|
device end-to-end responsibilities.
|
|
82
96
|
|
|
97
|
+
## Storage adapters
|
|
98
|
+
|
|
99
|
+
`setupStorageAdapterTestSuite` runs the storage adapter contract that deploy,
|
|
100
|
+
patch, the Console, the server and `storage prune` rely on: the canonical URI
|
|
101
|
+
of each key below the base path, streamed bodies, failed uploads that leave no
|
|
102
|
+
object, exact deletes, URIs of another bucket or protocol, download URLs, and
|
|
103
|
+
listing. A storage adapter's specs don't need `@hot-updater/server`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { setupStorageAdapterTestSuite } from "@hot-updater/test-utils";
|
|
107
|
+
|
|
108
|
+
import { myStorage } from "./myStorage";
|
|
109
|
+
|
|
110
|
+
setupStorageAdapterTestSuite({
|
|
111
|
+
name: "myStorage",
|
|
112
|
+
createStorage: async () => ({
|
|
113
|
+
storage: myStorage({ bucket: "bundles", basePath: "ota" }),
|
|
114
|
+
basePath: "ota",
|
|
115
|
+
}),
|
|
116
|
+
operations: ["put", "get", "getDownloadUrl", "exists", "delete"],
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`createStorage` returns the adapter over an empty bucket or base path for each
|
|
121
|
+
case, and the `cleanup` it may return runs after the case. `operations` lists
|
|
122
|
+
what the adapter implements: the suite fails when one of them is missing and
|
|
123
|
+
skips the cases of the others.
|
|
124
|
+
|
|
125
|
+
With `fetchDownloadUrls: true`, the suite downloads every `http(s)` URL
|
|
126
|
+
`getDownloadUrl` returns and requires the object's bytes, for a spec against a
|
|
127
|
+
real bucket or emulator whose URLs the test can reach.
|
|
128
|
+
|
|
83
129
|
## Plugins and servers
|
|
84
130
|
|
|
85
|
-
- `createPluginTestHarness` runs one server plugin
|
|
86
|
-
|
|
131
|
+
- `createPluginTestHarness` runs one server plugin on `createEngine` from
|
|
132
|
+
`@hot-updater/plugin-core`, the engine `createHotUpdater` runs plugins on, on
|
|
133
|
+
a memory adapter by default, and measures what each API call reads.
|
|
87
134
|
- `setupReleaseCatalogTestSuite` with `createHttpTestClient` tests a running
|
|
88
135
|
server by URL. Use `createHandlerHttpTestClient` inside a Workers Vitest pool.
|
|
89
136
|
- Server processes can import `createReleaseCatalogTestStorage` from
|
|
90
137
|
`@hot-updater/test-utils/node` to install the suites' storage fixtures.
|
|
91
138
|
|
|
139
|
+
## Client plugins
|
|
140
|
+
|
|
141
|
+
`@hot-updater/test-utils/react-native` runs client plugins the way the app
|
|
142
|
+
does. `setupClientPlugin(plugin, options)` and
|
|
143
|
+
`setupClientPlugins(plugins, options)` set plugins up as `HotUpdater.init`
|
|
144
|
+
does, on the plugin host from `@hot-updater/protocol`, the one the SDK runs,
|
|
145
|
+
against an in-memory device and a server the test answers. The entry loads
|
|
146
|
+
neither React Native nor Vitest, so plugin specs run in plain Node under
|
|
147
|
+
Vitest or Jest when the plugin imports `defineClientPlugin` from
|
|
148
|
+
`@hot-updater/protocol`; the SDK's root entry loads React Native.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { setupClientPlugin } from "@hot-updater/test-utils/react-native";
|
|
152
|
+
|
|
153
|
+
const runtime = setupClientPlugin(myPlugin(), {
|
|
154
|
+
requestHeaders: { "x-api-key": "client-key" },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
runtime.hooks.onAppReady(launch);
|
|
158
|
+
await runtime.settled();
|
|
159
|
+
|
|
160
|
+
expect(runtime.requests[0]?.json()).toEqual(expectedEvent);
|
|
161
|
+
expect(runtime.errors).toEqual([]);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`hooks` calls each hook as the SDK does, without waiting for it, and
|
|
165
|
+
`settled()` waits for what the hooks started. `requests` records what plugins
|
|
166
|
+
sent with `context.fetch`, and the `respond` option answers each request,
|
|
167
|
+
`204 No Content` by default. `errors` holds what the SDK reports to `onError`.
|
|
168
|
+
`storage` is the device's plugin storage: pass it to the next setup to start
|
|
169
|
+
the plugins again on the same device, or seed one with `createTestStorage()`.
|
|
170
|
+
|
|
92
171
|
See the
|
|
93
|
-
[
|
|
94
|
-
|
|
172
|
+
[storage adapter guide](https://hot-updater.dev/docs/storage-adapters/custom-storage#test-the-adapter),
|
|
173
|
+
the
|
|
174
|
+
[database adapter guide](https://hot-updater.dev/docs/database-adapters/custom-database),
|
|
175
|
+
and the
|
|
176
|
+
[client plugin guide](https://hot-updater.dev/docs/react-native-api/client-plugins#test-the-plugin)
|
|
177
|
+
for the contracts and complete specs.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ArtifactInfo, Bundle
|
|
2
|
-
import { DatabaseAdapter,
|
|
3
|
-
import {
|
|
1
|
+
import { ArtifactInfo, Bundle } from "@hot-updater/protocol";
|
|
2
|
+
import { AnyHotUpdaterPlugin, BundlePatchRow, BundleRow, ChannelRow, ClientAuth, DatabaseAdapter, DeployReleasePolicy, EngineDatabase, HotUpdaterCoreApi, HotUpdaterDatabase, KeyValueStore, ModuleSchema, PhysicalTable, PluginApis, ReadMeasurement, ReleaseRow, StorageAdapter, StorageOperation } from "@hot-updater/plugin-core";
|
|
3
|
+
import { BundleEventRow, InsightsModel } from "@hot-updater/plugin-insights/server";
|
|
4
4
|
//#region src/httpTestClient.d.ts
|
|
5
5
|
type HttpTestRequestInit = NonNullable<ConstructorParameters<typeof Request>[1]>;
|
|
6
6
|
type HttpTestRequest = (path: string, init?: HttpTestRequestInit) => Promise<Response>;
|
|
@@ -128,7 +128,7 @@ type DatabaseTestSuiteOptions<TDatabase extends EngineDatabase = EngineDatabase>
|
|
|
128
128
|
*/
|
|
129
129
|
readonly createHttpClient: (options: {
|
|
130
130
|
readonly database: TDatabase;
|
|
131
|
-
readonly storage: readonly
|
|
131
|
+
readonly storage: readonly StorageAdapter[];
|
|
132
132
|
}) => HttpTestServer | Promise<HttpTestServer>;
|
|
133
133
|
/** The suites of the server plugins the provider opts in to test. */
|
|
134
134
|
readonly plugins?: readonly DatabasePluginTestSuite<TDatabase>[];
|
|
@@ -175,9 +175,44 @@ declare const conformanceExpiring: PhysicalTable;
|
|
|
175
175
|
*/
|
|
176
176
|
declare const setupDatabaseAdapterConformanceSuite: (options: DatabaseAdapterConformanceOptions) => void;
|
|
177
177
|
//#endregion
|
|
178
|
+
//#region src/setupStorageAdapterTestSuite.d.ts
|
|
179
|
+
interface StorageAdapterTestSuiteOptions {
|
|
180
|
+
readonly name: string;
|
|
181
|
+
/**
|
|
182
|
+
* Returns the adapter over an empty bucket or base path. Every case creates
|
|
183
|
+
* its own and calls `cleanup` after it.
|
|
184
|
+
*/
|
|
185
|
+
readonly createStorage: () => Promise<{
|
|
186
|
+
readonly storage: StorageAdapter;
|
|
187
|
+
/**
|
|
188
|
+
* The base path the adapter stores keys below. With it, every URI `put`
|
|
189
|
+
* returns must have the key `<basePath>/<key>`.
|
|
190
|
+
*/
|
|
191
|
+
readonly basePath?: string;
|
|
192
|
+
readonly cleanup?: () => Promise<void>;
|
|
193
|
+
}>;
|
|
194
|
+
/**
|
|
195
|
+
* The operations the adapter implements. The suite requires them, with
|
|
196
|
+
* `put` and `get`, and skips the cases of every other operation. Without
|
|
197
|
+
* it, the suite skips the cases of the operations the adapter lacks.
|
|
198
|
+
*/
|
|
199
|
+
readonly operations?: readonly StorageOperation[];
|
|
200
|
+
/**
|
|
201
|
+
* Fetches every `http(s)` URL `getDownloadUrl` returns and requires the
|
|
202
|
+
* object's bytes. Set it when the adapter runs against a bucket or emulator
|
|
203
|
+
* whose URLs the test can reach.
|
|
204
|
+
*/
|
|
205
|
+
readonly fetchDownloadUrls?: boolean;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The storage adapter contract that deploy, patch, the Console, the server,
|
|
209
|
+
* and `storage prune` rely on. Every official storage adapter runs it.
|
|
210
|
+
*/
|
|
211
|
+
declare const setupStorageAdapterTestSuite: (options: StorageAdapterTestSuiteOptions) => void;
|
|
212
|
+
//#endregion
|
|
178
213
|
//#region src/sqlRowsExamined.d.ts
|
|
179
214
|
/**
|
|
180
|
-
* Rows a SQL database examined for the reads a
|
|
215
|
+
* Rows a SQL database examined for the reads a database adapter made: what
|
|
181
216
|
* read-budget suites compare with the rows the adapter read.
|
|
182
217
|
*/
|
|
183
218
|
interface RowsExamined {
|
|
@@ -237,72 +272,17 @@ declare const mysqlRowsExamined: (query: SqlQuery) => {
|
|
|
237
272
|
};
|
|
238
273
|
//#endregion
|
|
239
274
|
//#region src/setupReadBudgetTestSuite.d.ts
|
|
240
|
-
/** Core's API with the client routes' reads, which the public API leaves out. */
|
|
241
|
-
type ReadBudgetCore = HotUpdaterCoreApi & {
|
|
242
|
-
getReleaseCatalog(input: {
|
|
243
|
-
readonly strategy: "APP_VERSION";
|
|
244
|
-
readonly platform: "ios" | "android";
|
|
245
|
-
readonly channelKey: string;
|
|
246
|
-
readonly appVersion: string;
|
|
247
|
-
}): Promise<ReleaseCatalog | null>;
|
|
248
|
-
getArtifactInfo(targetBundleId: string, currentBundleId: string, artifactProtocolVersion: 1): Promise<ArtifactInfo | null>;
|
|
249
|
-
latestReleaseId(scopeKey: string): Promise<string | null>;
|
|
250
|
-
};
|
|
251
|
-
/** How core reads bundle manifests and resolves file URLs. */
|
|
252
|
-
interface ReadBudgetStorage {
|
|
253
|
-
readonly readStorageText?: (storageUri: string) => Promise<string | null>;
|
|
254
|
-
readonly resolveFileUrl: (storageUri: string | null) => Promise<string | null>;
|
|
255
|
-
}
|
|
256
|
-
/** What `createMeasuredDatabase` returns: core and the plugins on one engine in verify mode. */
|
|
257
|
-
interface ReadBudgetDatabase {
|
|
258
|
-
readonly core: ReadBudgetCore;
|
|
259
|
-
readonly api: Readonly<Record<string, unknown>>;
|
|
260
|
-
readonly clientAuth?: {
|
|
261
|
-
authenticate(headers: Headers): Promise<boolean>;
|
|
262
|
-
};
|
|
263
|
-
measureReads<T>(read: () => Promise<T>): Promise<{
|
|
264
|
-
readonly result: T;
|
|
265
|
-
readonly adapter: DatabaseReadCount;
|
|
266
|
-
readonly engine: {
|
|
267
|
-
readonly calls: number;
|
|
268
|
-
readonly rows: number;
|
|
269
|
-
};
|
|
270
|
-
}>;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* What the suite takes from `@hot-updater/server`; pass those exports, which
|
|
274
|
-
* keeps test-utils free of a dependency on the server.
|
|
275
|
-
*/
|
|
276
|
-
interface ReadBudgetServer {
|
|
277
|
-
/** `createMeasuredDatabase` from `@hot-updater/server/db`. */
|
|
278
|
-
createMeasuredDatabase(adapter: DatabaseAdapter, plugins: readonly unknown[], options: {
|
|
279
|
-
readonly now: () => number;
|
|
280
|
-
readonly storage: ReadBudgetStorage;
|
|
281
|
-
}): ReadBudgetDatabase;
|
|
282
|
-
/** `toolingTargetOf` from `@hot-updater/server/database`: core's and the plugins' tables to create. */
|
|
283
|
-
toolingTargetOf(plugins: readonly unknown[]): {
|
|
284
|
-
readonly schema: {
|
|
285
|
-
readonly tables: readonly PhysicalTable[];
|
|
286
|
-
};
|
|
287
|
-
};
|
|
288
|
-
/** `[insights(), apiKeys()]`, the plugins whose reads have budgets. */
|
|
289
|
-
readonly plugins: readonly unknown[];
|
|
290
|
-
/** `targetBaseCandidateKey` from `@hot-updater/server/db`. */
|
|
291
|
-
targetBaseCandidateKey(target: {
|
|
292
|
-
readonly channel: string;
|
|
293
|
-
readonly platform: "ios" | "android";
|
|
294
|
-
readonly fingerprintHash: string | null;
|
|
295
|
-
readonly appVersion: string | null;
|
|
296
|
-
}): string | null;
|
|
297
|
-
}
|
|
298
275
|
interface ReadBudgetAdapterContext {
|
|
276
|
+
/**
|
|
277
|
+
* Core's tables, those of the plugins the suite measures, and the settings
|
|
278
|
+
* table, where the retention passes keep their lease.
|
|
279
|
+
*/
|
|
299
280
|
readonly tables: readonly PhysicalTable[];
|
|
300
281
|
/** Test-only: the adapter must fetch at most this many rows per native page. */
|
|
301
282
|
readonly nativePageSize: number;
|
|
302
283
|
}
|
|
303
284
|
interface ReadBudgetSuiteOptions {
|
|
304
285
|
readonly name: string;
|
|
305
|
-
readonly server: ReadBudgetServer;
|
|
306
286
|
/** Returns an adapter over freshly created, empty tables. */
|
|
307
287
|
readonly createAdapter: (context: ReadBudgetAdapterContext) => Promise<{
|
|
308
288
|
readonly adapter: DatabaseAdapter;
|
|
@@ -317,44 +297,18 @@ interface ReadBudgetSuiteOptions {
|
|
|
317
297
|
}>;
|
|
318
298
|
}
|
|
319
299
|
/**
|
|
320
|
-
* The read-budget suite (PRD S4) on one backend: core and the
|
|
321
|
-
* plugins
|
|
322
|
-
* two rows. Every API in the read-budget list reads
|
|
323
|
-
* adapter and at the engine; with `examined`, the
|
|
324
|
-
* rows than the adapter read, within the backend's
|
|
300
|
+
* The read-budget suite (PRD S4) on one backend: core and the server's
|
|
301
|
+
* built-in plugins on `createMeasuredDatabase` over the backend's adapter,
|
|
302
|
+
* native pages capped at two rows. Every API in the read-budget list reads
|
|
303
|
+
* exactly its budget at the adapter and at the engine; with `examined`, the
|
|
304
|
+
* database examines no more rows than the adapter read, within the backend's
|
|
305
|
+
* native multipliers. It loads `@hot-updater/server`, an optional peer.
|
|
325
306
|
*/
|
|
326
307
|
declare const setupReadBudgetTestSuite: (options: ReadBudgetSuiteOptions) => void;
|
|
327
308
|
//#endregion
|
|
328
309
|
//#region src/setupAggregateBatchingTestSuite.d.ts
|
|
329
|
-
interface Module {
|
|
330
|
-
readonly id: string;
|
|
331
|
-
readonly schema: object;
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* What the suite takes from `@hot-updater/server/database`; pass that
|
|
335
|
-
* module, which keeps test-utils free of a dependency on the server.
|
|
336
|
-
*/
|
|
337
|
-
interface AggregateBatchingEngine {
|
|
338
|
-
resolveSchema(modules: readonly Module[]): {
|
|
339
|
-
readonly tables: readonly PhysicalTable[];
|
|
340
|
-
};
|
|
341
|
-
readonly aggregateBatchingModule: Module;
|
|
342
|
-
createDatabaseEngine(options: {
|
|
343
|
-
readonly adapter: DatabaseAdapter;
|
|
344
|
-
readonly schema: never;
|
|
345
|
-
readonly batching?: AggregateBatching;
|
|
346
|
-
readonly retry?: {
|
|
347
|
-
readonly attempts?: number;
|
|
348
|
-
};
|
|
349
|
-
}): {
|
|
350
|
-
database(module: Module): unknown;
|
|
351
|
-
flush(): Promise<void>;
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
310
|
interface AggregateBatchingSuiteOptions {
|
|
355
311
|
readonly name: string;
|
|
356
|
-
/** `import * as engine from "@hot-updater/server/database"`. */
|
|
357
|
-
readonly engine: AggregateBatchingEngine;
|
|
358
312
|
/** Returns an adapter over an empty store; each case calls it once. */
|
|
359
313
|
readonly createAdapter: () => Promise<{
|
|
360
314
|
readonly adapter: DatabaseAdapter;
|
|
@@ -368,7 +322,7 @@ interface AggregateBatchingSuiteOptions {
|
|
|
368
322
|
* writes with each transaction and a compaction merges once, and a memory
|
|
369
323
|
* buffer, read back exactly as transactional writes read.
|
|
370
324
|
*/
|
|
371
|
-
declare const setupAggregateBatchingTestSuite: ({ name,
|
|
325
|
+
declare const setupAggregateBatchingTestSuite: ({ name, createAdapter, oversizedRows }: AggregateBatchingSuiteOptions) => void;
|
|
372
326
|
//#endregion
|
|
373
327
|
//#region src/runContentionHarness.d.ts
|
|
374
328
|
/** Delays every adapter call by `ms`, like a round trip to a remote database. */
|
|
@@ -396,40 +350,48 @@ interface ContentionReport {
|
|
|
396
350
|
*/
|
|
397
351
|
declare const runContentionHarness: (options: ContentionOptions) => Promise<ContentionReport>;
|
|
398
352
|
//#endregion
|
|
399
|
-
//#region src/
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
readonly
|
|
403
|
-
readonly
|
|
353
|
+
//#region src/createMeasuredDatabase.d.ts
|
|
354
|
+
/** How core reads bundle manifests and resolves the file URLs of `storage://` URIs. */
|
|
355
|
+
interface MeasuredDatabaseStorage {
|
|
356
|
+
readonly readStorageText?: (storageUri: string) => Promise<string | null>;
|
|
357
|
+
readonly resolveFileUrl: (storageUri: string) => Promise<string>;
|
|
404
358
|
}
|
|
405
|
-
interface
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
359
|
+
interface MeasuredDatabaseOptions {
|
|
360
|
+
/**
|
|
361
|
+
* The plugins' clock, which they read through `ctx.now`. Core's
|
|
362
|
+
* timestamps and the retention passes run on `Date.now`.
|
|
363
|
+
*/
|
|
364
|
+
readonly now?: () => number;
|
|
365
|
+
/**
|
|
366
|
+
* The storage of `storage://` URIs, for reads that resolve them, such as
|
|
367
|
+
* artifact resolution; without it those reads fail.
|
|
368
|
+
*/
|
|
369
|
+
readonly storage?: MeasuredDatabaseStorage;
|
|
370
|
+
}
|
|
371
|
+
/** Core and the plugins' APIs on `createHotUpdater`, with the database's read meter. */
|
|
372
|
+
interface MeasuredDatabase<TApi = Readonly<Record<string, unknown>>> {
|
|
373
|
+
readonly core: HotUpdaterCoreApi;
|
|
374
|
+
readonly api: TApi;
|
|
375
|
+
/** The client-route policy, when a plugin provides one. */
|
|
376
|
+
readonly clientAuth?: ClientAuth;
|
|
377
|
+
/** Runs `read` and reports what it read at the adapter and at the engine. */
|
|
378
|
+
measureReads<T>(read: () => Promise<T>): Promise<ReadMeasurement<T>>;
|
|
415
379
|
}
|
|
416
380
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
381
|
+
* Core and the plugins over `adapter` as `createHotUpdater` runs them: what
|
|
382
|
+
* read-budget suites measure. The database is `meterReads` over
|
|
383
|
+
* `verifyAdapter(adapter)`, so every adapter call is checked against the
|
|
384
|
+
* contract and `measureReads` reports each call's reads; the adapter runs
|
|
385
|
+
* as given, without the schema fence. It loads `@hot-updater/server`, an
|
|
386
|
+
* optional peer, when called.
|
|
419
387
|
*/
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
};
|
|
424
|
-
createDatabaseEngine(options: {
|
|
425
|
-
readonly adapter: DatabaseAdapter;
|
|
426
|
-
readonly schema: never;
|
|
427
|
-
readonly verify?: boolean;
|
|
428
|
-
}): HarnessEngine;
|
|
429
|
-
}
|
|
388
|
+
declare const createMeasuredDatabase: <const TPlugins extends readonly AnyHotUpdaterPlugin[]>(adapter: DatabaseAdapter, plugins: TPlugins, options?: MeasuredDatabaseOptions) => Promise<MeasuredDatabase<PluginApis<TPlugins>>>;
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/createPluginTestHarness.d.ts
|
|
430
391
|
interface HarnessPlugin {
|
|
431
392
|
readonly id: string;
|
|
432
|
-
readonly
|
|
393
|
+
readonly schemaVersion: string;
|
|
394
|
+
readonly schema: ModuleSchema;
|
|
433
395
|
/** `false` keeps the declared table names, as `createHotUpdater` does. */
|
|
434
396
|
readonly namespace?: false;
|
|
435
397
|
init(context: never): {
|
|
@@ -437,36 +399,111 @@ interface HarnessPlugin {
|
|
|
437
399
|
};
|
|
438
400
|
}
|
|
439
401
|
interface PluginTestHarnessOptions {
|
|
440
|
-
/** `import * as engine from "@hot-updater/server/database"`. */
|
|
441
|
-
readonly engine: PluginTestEngine;
|
|
442
402
|
/** Defaults to a fresh memory adapter. */
|
|
443
403
|
readonly adapter?: DatabaseAdapter;
|
|
444
404
|
/** The plugin's clock; `setNow` changes it later. */
|
|
445
405
|
readonly now?: () => number;
|
|
446
406
|
}
|
|
447
407
|
/**
|
|
448
|
-
* Runs one plugin the
|
|
449
|
-
*
|
|
408
|
+
* Runs one plugin on `createEngine`, the storage engine `createHotUpdater`
|
|
409
|
+
* runs its plugins on, so tests can call its API and measure what each call
|
|
410
|
+
* reads. Every adapter call is checked against the adapter contract
|
|
411
|
+
* (`verifyAdapter`), and `ctx.core` is empty: core's reads need a server.
|
|
412
|
+
* The engine gets the adapter without `prune`, as a store that expires rows
|
|
413
|
+
* itself would give it, so no retention pass adds to what a call reads and
|
|
414
|
+
* writes; rows stay past their retention.
|
|
450
415
|
*/
|
|
451
|
-
declare const createPluginTestHarness: <TPlugin extends HarnessPlugin>(plugin: TPlugin, options
|
|
416
|
+
declare const createPluginTestHarness: <TPlugin extends HarnessPlugin>(plugin: TPlugin, options?: PluginTestHarnessOptions) => Promise<{
|
|
452
417
|
api: ReturnType<TPlugin["init"]>["api"];
|
|
453
418
|
instance: ReturnType<TPlugin["init"]>;
|
|
454
|
-
/** The plugin's own database handle
|
|
455
|
-
db:
|
|
419
|
+
/** The plugin's own database handle. */
|
|
420
|
+
db: HotUpdaterDatabase<TPlugin["schema"]>;
|
|
456
421
|
adapter: DatabaseAdapter;
|
|
457
|
-
tables
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
422
|
+
/** The plugin's physical tables. */
|
|
423
|
+
tables: import("@hot-updater/plugin-core").PhysicalTable[];
|
|
424
|
+
/** Reads at the adapter and at the engine while `read` runs. */
|
|
425
|
+
measureReads: <T>(read: () => Promise<T>) => Promise<import("@hot-updater/plugin-core").ReadMeasurement<T>>;
|
|
426
|
+
setNow: (next: () => number) => void;
|
|
427
|
+
}>;
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/databaseTestFixtures.d.ts
|
|
430
|
+
declare const createChannelRowFixture: (name?: string) => ChannelRow;
|
|
431
|
+
declare const createBundleRowFixture: (suffix: string, _channel?: string) => BundleRow;
|
|
432
|
+
declare const createBundlePatchRowFixture: (suffix: string, bundleId: string, baseBundleId: string, orderIndex?: number) => BundlePatchRow;
|
|
433
|
+
declare const createReleaseRowFixture: (suffix: string, bundle: BundleRow, channel: ChannelRow) => ReleaseRow;
|
|
434
|
+
declare const createBundleFixture: (suffix: string, _channel?: string) => Bundle;
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/storeBundles.d.ts
|
|
437
|
+
/**
|
|
438
|
+
* Stores bundles and their patches with no release, as a database holds an
|
|
439
|
+
* artifact that no release uses. `deploy` always adds a release, and
|
|
440
|
+
* deleting a bundle's last release deletes the bundle, so tests store such
|
|
441
|
+
* bundles through the engine. A patch's base comes before it in `bundles`.
|
|
442
|
+
*/
|
|
443
|
+
declare const storeBundles: (database: EngineDatabase, bundles: readonly Bundle[]) => Promise<void>;
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/insights/expectInsightsIndex.d.ts
|
|
446
|
+
/** Allow committed secondary indexes to catch up; query failures still fail. */
|
|
447
|
+
declare const expectInsightsIndex: <T>(query: () => Promise<T>, expected: T) => Promise<void>;
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/insights/fixtures.d.ts
|
|
450
|
+
/** An applied update from install `suffix`, received at `receivedAtMs`. */
|
|
451
|
+
declare const createBundleEventRowFixture: (suffix: string, receivedAtMs: number) => Extract<BundleEventRow, {
|
|
452
|
+
type: "UPDATE_APPLIED" | "RECOVERED";
|
|
468
453
|
}>;
|
|
469
454
|
//#endregion
|
|
455
|
+
//#region src/insights/setupInsightsHttpTestSuite.d.ts
|
|
456
|
+
/**
|
|
457
|
+
* The Insights plugin's routes on a database: a client reports an event, and
|
|
458
|
+
* the admin routes read it back. The server must run `insights()`.
|
|
459
|
+
*/
|
|
460
|
+
declare const setupInsightsHttpTestSuite: (options: {
|
|
461
|
+
readonly getClient: () => HttpTestClient;
|
|
462
|
+
}) => void;
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/insights/setupInsightsModelTestSuite.d.ts
|
|
465
|
+
/**
|
|
466
|
+
* The Insights report contract on one database: pass the Insights plugin's
|
|
467
|
+
* model over it, `createInsightsModel(createHotUpdater({ database, plugins:
|
|
468
|
+
* [insights()], clientAccess: "public" }).api.insights)`, as `createDatabase`.
|
|
469
|
+
*/
|
|
470
|
+
declare const setupInsightsModelTestSuite: (lifecycle: DatabaseTestLifecycle<InsightsModel>) => void;
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/insights/index.d.ts
|
|
473
|
+
/**
|
|
474
|
+
* The Insights plugin's tests, for `setupDatabaseTestSuite`'s `plugins`: its
|
|
475
|
+
* HTTP routes on the provider's server, which must run `insights()`, and
|
|
476
|
+
* with `createModel` its report contract on the provider's database, such as
|
|
477
|
+
* `(database) => createInsightsModel(createHotUpdater({ database, plugins:
|
|
478
|
+
* [insights()], clientAccess: "public" }).api.insights)`.
|
|
479
|
+
*/
|
|
480
|
+
declare const insightsTestSuite: <TDatabase = EngineDatabase>(options?: {
|
|
481
|
+
readonly createModel?: (database: TDatabase) => InsightsModel;
|
|
482
|
+
}) => {
|
|
483
|
+
name: string;
|
|
484
|
+
register: (context: {
|
|
485
|
+
readonly getClient: () => HttpTestClient;
|
|
486
|
+
readonly getDatabase: () => TDatabase;
|
|
487
|
+
}) => void;
|
|
488
|
+
};
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/kvTestStore.d.ts
|
|
491
|
+
interface MemoryKeyValueStoreOptions {
|
|
492
|
+
/** Test-only: the most items one `query` page returns. */
|
|
493
|
+
readonly nativePageSize?: number;
|
|
494
|
+
readonly limits?: KeyValueStore["limits"];
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* An in-memory key-value store that behaves like DynamoDB where the adapter
|
|
498
|
+
* can tell: a write checks every condition against the state before it,
|
|
499
|
+
* refuses two ops on one item, and applies all ops or none.
|
|
500
|
+
*/
|
|
501
|
+
declare const createMemoryKeyValueStore: (options?: MemoryKeyValueStoreOptions) => KeyValueStore & {
|
|
502
|
+
readonly items: () => number;
|
|
503
|
+
/** Each item's expiry, as a store's native TTL would hold it, by `[pk, sk]` JSON. */
|
|
504
|
+
readonly expiries: () => ReadonlyMap<string, number>;
|
|
505
|
+
};
|
|
506
|
+
//#endregion
|
|
470
507
|
//#region src/setupReleaseCatalogTestSuite.d.ts
|
|
471
508
|
/**
|
|
472
509
|
* Server conformance through HTTP only, on admin API protocol 2. The caller
|
|
@@ -494,6 +531,6 @@ declare const releaseCatalogArtifact: (bundle: BundleRow) => ArtifactInfo;
|
|
|
494
531
|
/** Install these storage objects in the test server, independently of its DB. */
|
|
495
532
|
declare const RELEASE_CATALOG_STORAGE_FIXTURES: Readonly<Record<string, string>>;
|
|
496
533
|
/** Storage boundary shared by provider and separately spawned server tests. */
|
|
497
|
-
declare const createReleaseCatalogTestStorage: () =>
|
|
534
|
+
declare const createReleaseCatalogTestStorage: () => StorageAdapter;
|
|
498
535
|
//#endregion
|
|
499
|
-
export { AdminApiTestClient,
|
|
536
|
+
export { AdminApiTestClient, AggregateBatchingSuiteOptions, ContentionOptions, ContentionReport, DatabaseAdapterConformanceContext, DatabaseAdapterConformanceOptions, DatabasePluginTestSuite, type DatabaseTestLifecycle, DatabaseTestSuiteOptions, HttpTestClient, HttpTestHandlers, HttpTestRequest, HttpTestRequestInit, HttpTestServer, MeasuredDatabase, MeasuredDatabaseOptions, MeasuredDatabaseStorage, MemoryKeyValueStoreOptions, PluginTestHarnessOptions, RELEASE_CATALOG_DOWNLOAD_HASH, RELEASE_CATALOG_MANIFEST_URIS, RELEASE_CATALOG_STORAGE_FIXTURES, ReadBudgetAdapterContext, ReadBudgetSuiteOptions, RowsExamined, SqlExecutorLike, SqlQuery, SqlStatementLike, StorageAdapterTestSuiteOptions, conformanceCounters, conformanceExpiring, conformanceItems, createAdminApiTestClient, createBundleEventRowFixture, createBundleFixture, createBundlePatchRowFixture, createBundleRowFixture, createChannelRowFixture, createHandlerHttpTestClient, createHttpTestClient, createMeasuredDatabase, createMemoryKeyValueStore, createPluginTestHarness, createReleaseCatalogTestStorage, createReleaseRowFixture, expectInsightsIndex, insightsTestSuite, jsonRequest, mysqlRowsExamined, postgresRowsExamined, releaseCatalogArtifact, releaseCatalogDownloadUrl, runContentionHarness, setupAggregateBatchingTestSuite, setupBundleMethodsTestSuite, setupCoreAdminTestSuite, setupDatabaseAdapterConformanceSuite, setupDatabaseTestSuite, setupInsightsHttpTestSuite, setupInsightsModelTestSuite, setupReadBudgetTestSuite, setupReleaseCatalogTestSuite, setupSemverSatisfiesTestSuite, setupStorageAdapterTestSuite, startHttpTestServer, storeBundles, toDeployBundle, withAdapterLatency };
|