@hot-updater/cloudflare 1.0.0-rc.18 → 1.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/index.cjs +7 -9
  2. package/dist/index.d.cts +323 -35
  3. package/dist/index.d.mts +323 -35
  4. package/dist/index.mjs +2 -4
  5. package/dist/init/index.cjs +11768 -0
  6. package/dist/init/index.d.cts +5 -1
  7. package/dist/init/index.d.mts +5 -1
  8. package/dist/init/index.mjs +11761 -1
  9. package/dist/worker/index.cjs +4 -5
  10. package/dist/worker/index.d.cts +323 -35
  11. package/dist/worker/index.d.mts +323 -35
  12. package/dist/worker/index.mjs +2 -3
  13. package/package.json +8 -11
  14. package/src/d1Database.ts +5 -2
  15. package/src/d1Executor.ts +2 -2
  16. package/src/d1Schema.ts +3 -2
  17. package/src/r2S3Storage.ts +4 -4
  18. package/src/r2Storage.ts +2 -2
  19. package/src/r2WorkerStorage.ts +4 -4
  20. package/worker/dist/README.md +1 -1
  21. package/worker/dist/index.js +4804 -4720
  22. package/worker/dist/index.js.map +4 -4
  23. package/dist/iac/index.cjs +0 -11838
  24. package/dist/iac/index.d.cts +0 -5
  25. package/dist/iac/index.d.mts +0 -5
  26. package/dist/iac/index.mjs +0 -11830
  27. package/src/d1Database.spec.ts +0 -190
  28. package/src/d1Schema.spec.ts +0 -89
  29. package/src/d1TestDatabase.ts +0 -38
  30. package/src/r2Storage.spec.ts +0 -346
  31. package/src/r2WorkerStorage.spec.ts +0 -184
  32. package/src/utils/createWrangler.spec.ts +0 -75
  33. package/src/worker/d1Database.spec.ts +0 -59
  34. package/worker/src/d1.conformance.integration.spec.ts +0 -53
  35. package/worker/src/d1.readBudgets.integration.spec.ts +0 -85
  36. package/worker/src/databaseMigration.integration.spec.ts +0 -101
  37. package/worker/src/databasePlugins.integration.spec.ts +0 -318
  38. package/worker/src/index.integration.spec.ts +0 -182
  39. package/worker/src/readPlans.integration.spec.ts +0 -89
  40. package/worker/src/storagePlugin.integration.spec.ts +0 -59
  41. package/worker/src/testWorker.ts +0 -5
@@ -1,318 +0,0 @@
1
- import type {
2
- DeployReleasePolicy,
3
- EngineDatabase,
4
- StoragePlugin,
5
- } from "@hot-updater/plugin-core";
6
- import { createHotUpdater } from "@hot-updater/server";
7
- import { isMultiIndex, toolingTargetOf } from "@hot-updater/server/database";
8
- import {
9
- createInsightsModel,
10
- insights,
11
- } from "@hot-updater/server/plugins/insights";
12
- import {
13
- createBundleEventRowFixture,
14
- insightsTestSuite,
15
- } from "@hot-updater/server/plugins/insights/testing";
16
- import {
17
- createHandlerHttpTestClient,
18
- setupDatabaseTestSuite,
19
- } from "@hot-updater/test-utils";
20
- import { env } from "cloudflare:test";
21
- import {
22
- afterAll,
23
- beforeAll,
24
- beforeEach,
25
- describe,
26
- expect,
27
- inject,
28
- it,
29
- vi,
30
- } from "vitest";
31
-
32
- import {
33
- createBundleFixture,
34
- createBundleRowFixture,
35
- } from "../../../../packages/test-utils/src/databaseTestFixtures";
36
- import { d1Database } from "../../src/d1Database";
37
- import { plugins } from "../../src/plugins";
38
- import { d1Database as d1RuntimeDatabase } from "../../src/worker";
39
-
40
- const state = vi.hoisted<{ db: D1Database | undefined }>(() => ({
41
- db: undefined,
42
- }));
43
-
44
- class D1TestStateError extends Error {
45
- readonly name = "D1TestStateError";
46
- }
47
-
48
- const getDb = (): D1Database => {
49
- if (state.db === undefined) {
50
- throw new D1TestStateError();
51
- }
52
- return state.db;
53
- };
54
-
55
- vi.mock("cloudflare", () => ({
56
- default: class MockCloudflare {
57
- readonly d1 = {
58
- database: {
59
- query: async (
60
- _databaseId: string,
61
- input:
62
- | { readonly sql: string; readonly params?: readonly string[] }
63
- | {
64
- readonly batch: readonly {
65
- readonly sql: string;
66
- readonly params?: readonly string[];
67
- }[];
68
- },
69
- ) => {
70
- const statements =
71
- "batch" in input
72
- ? input.batch.map(({ sql, params }) =>
73
- getDb()
74
- .prepare(sql)
75
- .bind(...(params ?? [])),
76
- )
77
- : (() => {
78
- const params = input.params ?? [];
79
- let paramOffset = 0;
80
- return input.sql
81
- .split(";")
82
- .map((sql) => sql.trim())
83
- .filter(Boolean)
84
- .map((sql) => {
85
- const paramCount = sql.match(/\?/g)?.length ?? 0;
86
- const statement = getDb()
87
- .prepare(sql)
88
- .bind(
89
- ...params.slice(
90
- paramOffset,
91
- paramOffset + paramCount,
92
- ),
93
- );
94
- paramOffset += paramCount;
95
- return statement;
96
- });
97
- })();
98
- const results = await getDb().batch(statements);
99
- return {
100
- async *iterPages() {
101
- yield { result: results };
102
- },
103
- };
104
- },
105
- },
106
- };
107
- },
108
- }));
109
-
110
- /** Every data table of the managed server's migration: each model's table and the index tables of its multi-valued indexes. */
111
- const dataTables = toolingTargetOf(plugins).schema.tables.flatMap((table) => [
112
- table.name,
113
- ...table.indexes
114
- .filter((index) => isMultiIndex(table, index))
115
- .map((index) => `${table.name}__${index.name}`),
116
- ]);
117
-
118
- /** Empties every data table; the settings rows stay. */
119
- const reset = async (): Promise<void> => {
120
- await getDb().batch(
121
- dataTables.map((name) => getDb().prepare(`DELETE FROM "${name}"`)),
122
- );
123
- };
124
-
125
- /**
126
- * A public server with Insights on `database`, as the suite serves it. The
127
- * specs here reach core and the Insights API through it.
128
- */
129
- const serve = (options: {
130
- readonly database: EngineDatabase;
131
- readonly storage?: readonly StoragePlugin[];
132
- }) =>
133
- createHotUpdater({
134
- ...options,
135
- plugins: [insights()],
136
- clientAccess: "public",
137
- });
138
-
139
- const release = (channel: string, enabled: boolean): DeployReleasePolicy => ({
140
- channel,
141
- enabled,
142
- fingerprintHash: null,
143
- message: null,
144
- shouldForceUpdate: false,
145
- targetAppVersion: "1.0.0",
146
- });
147
-
148
- setupDatabaseTestSuite({
149
- createHttpClient: (options) =>
150
- createHandlerHttpTestClient(serve(options).handlers),
151
- plugins: [
152
- insightsTestSuite({
153
- createModel: (database: EngineDatabase) =>
154
- createInsightsModel(serve({ database }).api.insights),
155
- }),
156
- ],
157
- name: "cloudflare d1 http",
158
- migrate: async () => {
159
- state.db = env.DB;
160
- await getDb().prepare(inject("prepareSql")).run();
161
- },
162
- createDatabase: () =>
163
- d1Database({
164
- accountId: "account-id",
165
- cloudflareApiToken: "api-token",
166
- databaseId: "database-id",
167
- }),
168
- reset,
169
- dispose: () => undefined,
170
- });
171
-
172
- setupDatabaseTestSuite({
173
- createHttpClient: (options) =>
174
- createHandlerHttpTestClient(serve(options).handlers),
175
- plugins: [
176
- insightsTestSuite({
177
- createModel: (database: EngineDatabase) =>
178
- createInsightsModel(serve({ database }).api.insights),
179
- }),
180
- ],
181
- name: "cloudflare worker d1",
182
- migrate: () => undefined,
183
- createDatabase: () => d1RuntimeDatabase(env.DB),
184
- reset,
185
- dispose: () => {
186
- state.db = undefined;
187
- },
188
- });
189
-
190
- describe.each([
191
- {
192
- name: "cloudflare d1 http",
193
- createDatabase: () =>
194
- d1Database({
195
- accountId: "account-id",
196
- cloudflareApiToken: "api-token",
197
- databaseId: "database-id",
198
- }),
199
- },
200
- {
201
- name: "cloudflare worker d1",
202
- createDatabase: () => d1RuntimeDatabase(env.DB),
203
- },
204
- ])("$name Channel deletion", ({ createDatabase }) => {
205
- beforeAll(() => {
206
- state.db = env.DB;
207
- });
208
-
209
- beforeEach(async () => {
210
- await reset();
211
- });
212
-
213
- afterAll(() => {
214
- state.db = undefined;
215
- });
216
-
217
- it("deletes only empty channels", async () => {
218
- const { core } = serve({ database: createDatabase() });
219
- const channel = await core.ensureChannel("empty");
220
-
221
- await expect(core.deleteChannel(channel.id)).resolves.toEqual({
222
- deleted: true,
223
- });
224
- await expect(core.deleteChannel(channel.id)).resolves.toEqual({
225
- deleted: false,
226
- reason: "not_found",
227
- });
228
- });
229
-
230
- it("keeps failed event inserts invisible, then safely retries", async () => {
231
- const model = createInsightsModel(
232
- serve({ database: createDatabase() }).api.insights,
233
- );
234
- const event = createBundleEventRowFixture("9101", 100);
235
- const input = { event };
236
- await env.DB.prepare(`
237
- CREATE TRIGGER fail_insights_event BEFORE INSERT ON bundle_events
238
- BEGIN SELECT RAISE(ABORT, 'injected event failure'); END;
239
- `).run();
240
- try {
241
- // The engine reports a failed batch as ambiguous, with D1's error as its cause.
242
- await expect(model.recordEvent(input)).rejects.toMatchObject({
243
- cause: { message: expect.stringContaining("injected event failure") },
244
- });
245
- expect(
246
- (await env.DB.prepare("SELECT * FROM bundle_events").all()).results,
247
- ).toEqual([]);
248
- await expect(
249
- model.findLatestEvents({ installId: event.install_id }),
250
- ).resolves.toEqual([]);
251
- } finally {
252
- await env.DB.prepare("DROP TRIGGER fail_insights_event").run();
253
- }
254
- await env.DB.prepare(`
255
- CREATE TRIGGER fail_insights_head BEFORE INSERT ON bundle_event_heads
256
- BEGIN SELECT RAISE(ABORT, 'injected head failure'); END;
257
- `).run();
258
- try {
259
- // The engine reports a failed batch as ambiguous, with D1's error as its cause.
260
- await expect(model.recordEvent(input)).rejects.toMatchObject({
261
- cause: { message: expect.stringContaining("injected head failure") },
262
- });
263
- expect(
264
- (await env.DB.prepare("SELECT * FROM bundle_events").all()).results,
265
- ).toEqual([]);
266
- expect(
267
- (await env.DB.prepare("SELECT * FROM bundle_event_heads").all())
268
- .results,
269
- ).toEqual([]);
270
- } finally {
271
- await env.DB.prepare("DROP TRIGGER fail_insights_head").run();
272
- }
273
- await model.recordEvent(input);
274
- await model.recordEvent(input);
275
- await expect(
276
- model.findLatestEvents({ installId: event.install_id }),
277
- ).resolves.toEqual([input.event]);
278
- expect(
279
- (await env.DB.prepare("SELECT id FROM bundle_events").all()).results,
280
- ).toEqual([{ id: event.id }]);
281
- });
282
-
283
- it("refuses to delete a channel while a Release uses it", async () => {
284
- const { core } = serve({ database: createDatabase() });
285
- const channel = await core.ensureChannel("active");
286
- const bundle = createBundleFixture("902");
287
- await core.deploy([{ bundle, release: release(channel.name, true) }]);
288
-
289
- await expect(core.deleteChannel(channel.id)).resolves.toEqual({
290
- deleted: false,
291
- reason: "not_empty",
292
- });
293
- await expect(core.listChannels()).resolves.toEqual([channel]);
294
- await expect(core.getBundle(bundle.id)).resolves.toMatchObject({
295
- bundle: createBundleRowFixture("902"),
296
- });
297
- });
298
-
299
- it("deletes a channel once its last Release is deleted", async () => {
300
- const { core } = serve({ database: createDatabase() });
301
- const channel = await core.ensureChannel("retired");
302
- const bundle = createBundleFixture("903");
303
- const [deployed] = await core.deploy([
304
- { bundle, release: release(channel.name, false) },
305
- ]);
306
- const releaseId = deployed!.release!.id;
307
-
308
- await core.deleteRelease({ releaseId });
309
- await expect(core.deleteChannel(channel.id)).resolves.toEqual({
310
- deleted: true,
311
- });
312
- await expect(core.getRelease(releaseId)).resolves.toBeNull();
313
- await expect(core.getBundle(bundle.id)).resolves.toMatchObject({
314
- bundle: createBundleRowFixture("903"),
315
- });
316
- await expect(core.listChannels()).resolves.toEqual([]);
317
- });
318
- });
@@ -1,182 +0,0 @@
1
- import type { Bundle } from "@hot-updater/core";
2
- import { createHotUpdater } from "@hot-updater/server";
3
- import { createBundleEventRowFixture } from "@hot-updater/server/plugins/insights/testing";
4
- import { env } from "cloudflare:test";
5
- import { beforeAll, beforeEach, describe, expect, inject, it } from "vitest";
6
-
7
- import { d1Database, plugins } from "../../src/worker";
8
- import worker, { HOT_UPDATER_BASE_PATH } from "./index";
9
-
10
- declare module "vitest" {
11
- export interface ProvidedContext {
12
- prepareSql: string;
13
- }
14
- }
15
-
16
- declare module "cloudflare:test" {
17
- interface ProvidedEnv {
18
- DB: D1Database;
19
- BUCKET: R2Bucket;
20
- BUCKET_NAME: string;
21
- STORAGE_DOWNLOAD_URL_SIGNING_KEY: string;
22
- }
23
- }
24
-
25
- const PUBLIC_BASE_URL = "https://updates.example.com";
26
- const API_KEY = "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE";
27
-
28
- const toRuntimeBundle = (bundle: Bundle): Bundle => {
29
- return {
30
- ...bundle,
31
- manifestStorageUri: `r2://${env.BUCKET_NAME}/${bundle.id}/manifest.json`,
32
- assetBaseStorageUri: `r2://${env.BUCKET_NAME}/assets`,
33
- };
34
- };
35
-
36
- /** A server on the test database with the plugins the Worker runs. */
37
- const createSeedServer = () =>
38
- createHotUpdater({ database: d1Database(env.DB), plugins });
39
-
40
- const seedBundles = async (bundles: Bundle[]) => {
41
- const { core } = createSeedServer();
42
- // A deploy publishes into each scope at most once, so each bundle deploys alone.
43
- for (const bundle of bundles) {
44
- await core.deploy([
45
- {
46
- bundle: toRuntimeBundle(bundle),
47
- release: {
48
- channel: "production",
49
- enabled: true,
50
- fingerprintHash: null,
51
- message: "hello",
52
- shouldForceUpdate: false,
53
- targetAppVersion: "1.0",
54
- },
55
- },
56
- ]);
57
- }
58
- };
59
-
60
- describe.sequential("cloudflare worker runtime acceptance", () => {
61
- beforeAll(async () => {
62
- await env.DB.prepare(inject("prepareSql")).run();
63
- await createSeedServer().api.apiKeys.register({
64
- apiKey: API_KEY,
65
- name: "Runtime acceptance",
66
- });
67
- });
68
-
69
- beforeEach(async () => {
70
- await env.DB.prepare("DELETE FROM bundle_patches").run();
71
- await env.DB.prepare("DELETE FROM release_catalogs").run();
72
- await env.DB.prepare("DELETE FROM releases").run();
73
- await env.DB.prepare("DELETE FROM bundles").run();
74
- await env.DB.prepare("DELETE FROM channels").run();
75
- await env.DB.prepare("DELETE FROM bundle_totals").run();
76
- });
77
-
78
- it("serves unversioned Release Catalog routes from the worker entrypoint", async () => {
79
- expect(HOT_UPDATER_BASE_PATH).toBe("/");
80
- await seedBundles([
81
- {
82
- id: "00000000-0000-0000-0000-000000000001",
83
- platform: "ios",
84
- gitCommitHash: null,
85
- manifestStorageUri: "storage://unused/manifest.json",
86
- manifestFileHash: "manifest-hash",
87
- assetBaseStorageUri: "storage://assets",
88
- },
89
- ]);
90
-
91
- const unauthorized = await worker.fetch(
92
- new Request(
93
- `${PUBLIC_BASE_URL}/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0`,
94
- ),
95
- env,
96
- );
97
- expect(unauthorized.status).toBe(401);
98
-
99
- const response = await worker.fetch(
100
- new Request(
101
- `${PUBLIC_BASE_URL}/release-catalogs/app-version/ios/cHJvZHVjdGlvbg/1.0.0`,
102
- { headers: { "x-api-key": API_KEY } },
103
- ),
104
- env,
105
- );
106
-
107
- await expect(response.json()).resolves.toMatchObject({
108
- catalogId: expect.stringMatching(/^[0-9a-f-]{36}$/),
109
- releases: [{ bundleId: "00000000-0000-0000-0000-000000000001" }],
110
- });
111
- });
112
-
113
- it("does not support the legacy exact path", async () => {
114
- const response = await worker.fetch(
115
- new Request(`${PUBLIC_BASE_URL}/api/check-update`),
116
- env,
117
- );
118
-
119
- expect(response.status).toBe(404);
120
- await expect(response.json()).resolves.toEqual({
121
- error: "Not found",
122
- });
123
- });
124
-
125
- it("does not expose management routes from the worker entrypoint", async () => {
126
- const response = await worker.fetch(
127
- new Request(`${PUBLIC_BASE_URL}/admin/bundles`),
128
- env,
129
- );
130
-
131
- expect(response.status).toBe(404);
132
- await expect(response.json()).resolves.toEqual({
133
- error: "Not found",
134
- });
135
- });
136
-
137
- it("deletes rows past their retention after a write, with no cron trigger", async () => {
138
- const day = 86_400_000;
139
- const expired = createBundleEventRowFixture("9701", Date.now() - 100 * day);
140
- await createSeedServer().api.insights.recordEvent(expired);
141
- // The seed servers' writes hold the hourly lease; release it.
142
- await env.DB.prepare(
143
- "DELETE FROM private_hot_updater_settings WHERE key = 'retention.nextPassAt'",
144
- ).run();
145
-
146
- const reported = await worker.fetch(
147
- new Request(`${PUBLIC_BASE_URL}/events`, {
148
- method: "POST",
149
- headers: { "content-type": "application/json", "x-api-key": API_KEY },
150
- body: JSON.stringify({
151
- appVersion: "1.0",
152
- channel: "production",
153
- cohort: "default",
154
- fingerprintHash: null,
155
- fromBundleId: null,
156
- fromReleaseId: null,
157
- installId: "install-9702",
158
- platform: "ios",
159
- toBundleId: "00000000-0000-7000-8000-000000009702",
160
- toReleaseId: null,
161
- type: "UNCHANGED",
162
- updateStrategy: null,
163
- }),
164
- }),
165
- env,
166
- );
167
- expect(reported.status).toBe(204);
168
-
169
- const ids = async (table: string, column: string) =>
170
- (
171
- await env.DB.prepare(
172
- `SELECT ${column} AS id FROM ${table} ORDER BY ${column}`,
173
- ).all<{ id: string }>()
174
- ).results.map(({ id }) => id);
175
- // Events are kept 90 days; an installation's latest event, 13 months.
176
- await expect(ids("bundle_events", "id")).resolves.not.toContain(expired.id);
177
- await expect(ids("bundle_event_heads", "install_id")).resolves.toEqual([
178
- expired.install_id,
179
- "install-9702",
180
- ]);
181
- });
182
- });
@@ -1,89 +0,0 @@
1
- import { createHotUpdater } from "@hot-updater/server";
2
- import { insights } from "@hot-updater/server/plugins/insights";
3
- import { createBundleEventRowFixture } from "@hot-updater/server/plugins/insights/testing";
4
- import { env } from "cloudflare:test";
5
- import { expect, inject, it } from "vitest";
6
-
7
- import { createBundleFixture } from "../../../../packages/test-utils/src/databaseTestFixtures";
8
- import { d1Database } from "../../src/worker";
9
-
10
- it("reads only through declared indexes on D1, never a full scan or a sort", async () => {
11
- await env.DB.prepare(inject("prepareSql")).run();
12
- /** Each operation's reads, in the order the operations ran. */
13
- const reads = new Map<
14
- string,
15
- { readonly sql: string; readonly params: readonly unknown[] }[]
16
- >();
17
- let operation = "";
18
- const { core, api } = createHotUpdater({
19
- database: d1Database({
20
- prepare(sql) {
21
- return {
22
- bind(...params) {
23
- if (/^\s*SELECT\b/iu.test(sql)) {
24
- reads.get(operation)!.push({ sql, params });
25
- }
26
- return env.DB.prepare(sql).bind(...params);
27
- },
28
- };
29
- },
30
- batch: (statements) => env.DB.batch(statements as D1PreparedStatement[]),
31
- }),
32
- plugins: [insights()],
33
- clientAccess: "public",
34
- });
35
- /** Runs `run`, recording the reads it makes under `name`. */
36
- const measure = async <T>(name: string, run: () => Promise<T>) => {
37
- operation = name;
38
- reads.set(name, []);
39
- return run();
40
- };
41
-
42
- const event = createBundleEventRowFixture("11", 100);
43
- await measure("ensureChannel", () => core.ensureChannel("production"));
44
- const [deployed] = await measure("deploy", () =>
45
- core.deploy([
46
- {
47
- bundle: createBundleFixture("1"),
48
- release: {
49
- channel: "production",
50
- enabled: true,
51
- fingerprintHash: null,
52
- message: null,
53
- shouldForceUpdate: false,
54
- targetAppVersion: "1.0.0",
55
- },
56
- },
57
- ]),
58
- );
59
- await measure("insights.recordEvent", () => api.insights.recordEvent(event));
60
- await measure("getRelease", () => core.getRelease(deployed!.release!.id));
61
- await measure("listChannels", () => core.listChannels());
62
- await measure("insights.findLatestEvents", () =>
63
- api.insights.findLatestEvents({ installId: event.install_id }),
64
- );
65
- await measure("insights.countLatestEvents", () =>
66
- api.insights.countLatestEvents({
67
- platform: event.platform,
68
- channel: event.channel,
69
- sinceMs: 0,
70
- }),
71
- );
72
-
73
- for (const [name, statements] of reads) {
74
- expect(statements.length, name).toBeGreaterThan(0);
75
- for (const { sql, params } of statements) {
76
- const plan = await env.DB.prepare(`EXPLAIN QUERY PLAN ${sql}`)
77
- .bind(...params)
78
- .all<{ detail: string }>();
79
- const details = plan.results.map(({ detail }) => detail);
80
- expect(
81
- details.filter((detail) => /^SCAN \S+$/u.test(detail)),
82
- `${name}: ${sql}`,
83
- ).toEqual([]);
84
- expect(details.join("\n"), `${name}: ${sql}`).not.toContain(
85
- "TEMP B-TREE",
86
- );
87
- }
88
- }
89
- });
@@ -1,59 +0,0 @@
1
- import { env } from "cloudflare:test";
2
- import { afterEach, describe, expect, it } from "vitest";
3
-
4
- import { r2Storage } from "../../src/worker";
5
-
6
- const UNKNOWN_LENGTH_KEY = "storage-tests/unknown-length.txt";
7
- const KNOWN_LENGTH_KEY = "storage-tests/known-length.txt";
8
-
9
- const createChunkedBody = (...chunks: string[]) =>
10
- new ReadableStream<Uint8Array>({
11
- start(controller) {
12
- for (const chunk of chunks) {
13
- controller.enqueue(new TextEncoder().encode(chunk));
14
- }
15
- controller.close();
16
- },
17
- });
18
-
19
- const createStorage = () =>
20
- r2Storage({
21
- bucket: env.BUCKET,
22
- bucketName: env.BUCKET_NAME,
23
- downloadUrlSigningKey: env.STORAGE_DOWNLOAD_URL_SIGNING_KEY,
24
- });
25
-
26
- afterEach(async () => {
27
- await env.BUCKET.delete([UNKNOWN_LENGTH_KEY, KNOWN_LENGTH_KEY]);
28
- });
29
-
30
- describe("Cloudflare Worker R2 storage uploads", () => {
31
- it("writes exact bytes from an unknown-length stream", async () => {
32
- const storage = createStorage();
33
-
34
- await storage.put({
35
- key: UNKNOWN_LENGTH_KEY,
36
- body: createChunkedBody("unknown", "-length"),
37
- contentType: "text/plain",
38
- });
39
-
40
- await expect(
41
- env.BUCKET.get(UNKNOWN_LENGTH_KEY).then((item) => item?.text()),
42
- ).resolves.toBe("unknown-length");
43
- });
44
-
45
- it("streams known-length bytes through FixedLengthStream", async () => {
46
- const storage = createStorage();
47
-
48
- await storage.put({
49
- key: KNOWN_LENGTH_KEY,
50
- body: createChunkedBody("known", "-length"),
51
- contentLength: 12,
52
- contentType: "text/plain",
53
- });
54
-
55
- await expect(
56
- env.BUCKET.get(KNOWN_LENGTH_KEY).then((item) => item?.text()),
57
- ).resolves.toBe("known-length");
58
- });
59
- });
@@ -1,5 +0,0 @@
1
- export default {
2
- fetch() {
3
- return new Response("ok");
4
- },
5
- };