@hot-updater/test-utils 1.1.4 → 1.2.0-rc.1

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/dist/index.d.mts CHANGED
@@ -64,8 +64,10 @@ declare const setupBundleMethodsTestSuite: (options: {
64
64
  //#region src/setupCoreAdminTestSuite.d.ts
65
65
  /**
66
66
  * Core's operations through admin API protocol 2 that the bundle and catalog
67
- * suites leave out: channels, deploy refusals, promotion, preflights, and a
68
- * bundle's patch children. Each test deletes what it wrote.
67
+ * suites leave out: channels, deploy refusals, promotion, preflights, a
68
+ * bundle's patch children, and the native build registry. Each test deletes
69
+ * what it wrote, except a native build, which the registry keeps: its IDs
70
+ * and channel are the test's own.
69
71
  */
70
72
  declare const setupCoreAdminTestSuite: (options: {
71
73
  readonly getClient: () => HttpTestClient;
package/dist/index.mjs CHANGED
@@ -237,8 +237,10 @@ const setupBundleMethodsTestSuite = (options) => {
237
237
  //#region src/setupCoreAdminTestSuite.ts
238
238
  /**
239
239
  * Core's operations through admin API protocol 2 that the bundle and catalog
240
- * suites leave out: channels, deploy refusals, promotion, preflights, and a
241
- * bundle's patch children. Each test deletes what it wrote.
240
+ * suites leave out: channels, deploy refusals, promotion, preflights, a
241
+ * bundle's patch children, and the native build registry. Each test deletes
242
+ * what it wrote, except a native build, which the registry keeps: its IDs
243
+ * and channel are the test's own.
242
244
  */
243
245
  const setupCoreAdminTestSuite = (options) => {
244
246
  describe("Core admin API", () => {
@@ -352,6 +354,107 @@ const setupCoreAdminTestSuite = (options) => {
352
354
  expect(await adminJson(catalogPath)).toEqual(before);
353
355
  expect((await adminJson(`/releases/${release.id}`)).data).toEqual(release);
354
356
  });
357
+ it("registers native builds, finds the newest of a scope, and keeps their built-in bundles", async () => {
358
+ const channel = `native-${namespace}`;
359
+ const registration = (suffix, id = bundleRow(`8${suffix}`).id, buildKey = null) => {
360
+ const builtIn = bundleRow(suffix);
361
+ return admin("/native-builds", jsonRequest("POST", {
362
+ id,
363
+ platform: "ios",
364
+ channel,
365
+ fingerprintHash: "native-fingerprint",
366
+ appVersion: null,
367
+ minBundleId: builtIn.id,
368
+ gitCommitHash: null,
369
+ buildKey,
370
+ buildNumber: suffix,
371
+ artifact: {
372
+ kind: "ipa",
373
+ storageUri: `storage://native-builds/ios/${suffix}/App.ipa`,
374
+ byteSize: 1e3
375
+ },
376
+ embeddedBundle: {
377
+ manifestStorageUri: builtIn.manifest_storage_uri,
378
+ manifestFileHash: builtIn.manifest_file_hash,
379
+ assetBaseStorageUri: builtIn.asset_base_storage_uri
380
+ }
381
+ }));
382
+ };
383
+ const register = async (suffix, buildKey) => {
384
+ const response = await registration(suffix, bundleRow(`8${suffix}`).id, buildKey);
385
+ expect(response.status).toBe(201);
386
+ return (await response.json()).data;
387
+ };
388
+ const scope = (query) => `/native-builds/latest?platform=ios&channel=${encodeURIComponent(channel)}&${query}`;
389
+ const before = await adminJson("/bundles?limit=1&total=true");
390
+ const keyed = `nbk1:${"a".repeat(64)}`;
391
+ const keyedBuild = await register("60", keyed);
392
+ const newest = await register("61");
393
+ const again = await registration("61", bundleRow("862").id);
394
+ const refusedDelete = await admin("/bundles/delete", jsonRequest("POST", { ids: [bundleRow("61").id] }));
395
+ expect((await adminJson(scope("fingerprintHash=native-fingerprint"))).data).toEqual(newest);
396
+ expect((await adminJson(scope("appVersion=1.0.0"))).data).toBeNull();
397
+ expect(keyedBuild.build_key).toBe(keyed);
398
+ expect(newest.build_key).toBeNull();
399
+ expect(newest.build_number).toBe("61");
400
+ expect((await adminJson(scope(`fingerprintHash=native-fingerprint&buildKey=${keyed}`))).data).toEqual(keyedBuild);
401
+ expect((await adminJson(scope(`fingerprintHash=native-fingerprint&buildKey=nbk1:${"b".repeat(64)}`))).data).toBeNull();
402
+ const badKey = await admin(scope("fingerprintHash=native-fingerprint&buildKey=not-a-key"));
403
+ expect(badKey.status).toBe(400);
404
+ await badKey.text();
405
+ expect((await adminJson(`/native-builds/${newest.id}`)).data).toEqual(newest);
406
+ expect((await adminJson(`/native-builds/${bundleRow("863").id}`)).data).toBeNull();
407
+ const channelRow = (await adminJson(`/channels?name=${encodeURIComponent(channel)}`)).data;
408
+ const ofChannel = (channelId, query = "") => adminJson(`/native-builds?channelId=${encodeURIComponent(channelId)}&order=desc&${query}`);
409
+ const firstPage = await ofChannel(channelRow.id, "limit=1");
410
+ expect(firstPage).toEqual({
411
+ data: [newest],
412
+ next: newest.id
413
+ });
414
+ expect(await ofChannel(channelRow.id, `limit=1&cursor=${newest.id}`)).toEqual({
415
+ data: [keyedBuild],
416
+ next: keyedBuild.id
417
+ });
418
+ expect((await ofChannel(channelRow.id, "limit=10")).data).toEqual([newest, keyedBuild]);
419
+ const other = await api.ensureChannel(`native-other-${namespace}`);
420
+ expect((await ofChannel(other.id, "limit=10")).data).toEqual([]);
421
+ await adminJson(`/channels/${other.id}`, jsonRequest("DELETE"));
422
+ expect((await ofChannel(other.id, "limit=10")).data).toEqual([]);
423
+ const submitted = await adminJson(`/native-builds/${newest.id}/submission`, jsonRequest("POST", {
424
+ destination: "testflight",
425
+ version: "1.0.0 (7)"
426
+ }));
427
+ expect(submitted.data).toMatchObject({
428
+ id: newest.id,
429
+ submitted_to: "testflight",
430
+ submitted_version: "1.0.0 (7)"
431
+ });
432
+ expect((await adminJson(`/native-builds/${newest.id}`)).data).toEqual(submitted.data);
433
+ expect((await adminJson(`/native-builds/${bundleRow("863").id}/submission`, jsonRequest("POST", {
434
+ destination: "testflight",
435
+ version: "1.0.0 (7)"
436
+ }))).data).toBeNull();
437
+ const wrongStore = await admin(`/native-builds/${newest.id}/submission`, jsonRequest("POST", {
438
+ destination: "play-internal",
439
+ version: "1.0.0 (7)"
440
+ }));
441
+ expect(wrongStore.status).toBe(400);
442
+ await wrongStore.text();
443
+ expect(again.status).toBe(409);
444
+ expect(await again.json()).toMatchObject({ code: "NATIVE_BUILD_EXISTS" });
445
+ expect(refusedDelete.status).toBe(409);
446
+ await refusedDelete.text();
447
+ expect((await adminJson(`/bundles/${bundleRow("61").id}`)).data).toEqual({
448
+ bundle: {
449
+ ...bundleRow("61"),
450
+ metadata: {}
451
+ },
452
+ patches: [],
453
+ childCount: 0
454
+ });
455
+ const after = await adminJson("/bundles?limit=1&total=true");
456
+ expect(after.total).toBe(before.total);
457
+ });
355
458
  it("lists the patches that start from a bundle", async () => {
356
459
  const base = bundleRow("50");
357
460
  const target = bundleRow("51");
@@ -2919,6 +3022,14 @@ const scopeKey = createReleaseCatalogScopeKey({
2919
3022
  platform: "ios",
2920
3023
  strategy: "APP_VERSION"
2921
3024
  });
3025
+ const nativeBuildScope = {
3026
+ platform: "ios",
3027
+ channel: "production",
3028
+ fingerprintHash: "native-fingerprint",
3029
+ appVersion: null
3030
+ };
3031
+ /** The native build key the older of the two native builds is registered with. */
3032
+ const nativeBuildKey = `nbk1:${"0".repeat(64)}`;
2922
3033
  const bundleEvents = {
2923
3034
  kind: "bundle",
2924
3035
  platform: "ios",
@@ -2935,7 +3046,7 @@ const storage = {
2935
3046
  resolveFileUrl: async (storageUri) => `https://storage.example.com/${encodeURIComponent(storageUri)}`
2936
3047
  };
2937
3048
  /**
2938
- * Core's bundles, releases, channels, and a client API key, and 26 Insights
3049
+ * Core's bundles, releases, channels, a client API key, and 26 Insights
2939
3050
  * events: installs 1–24 ten minutes apart from T0, then install 1 again two
2940
3051
  * days later, back on bundle-a and then on bundle-b; and the day of history
2941
3052
  * before T0, as a production database holds, so a read that scans beyond its
@@ -2981,6 +3092,23 @@ const seed = async (database) => {
2981
3092
  await deploy(bundleOf("202", "android"));
2982
3093
  await deploy(bundleOf("091"), "staging");
2983
3094
  await deploy(bundleOf("092"), "staging");
3095
+ for (const suffix of ["011", "012"]) await core.registerNativeBuild({
3096
+ ...nativeBuildScope,
3097
+ id: createBundleFixture(`8${suffix}`).id,
3098
+ minBundleId: createBundleFixture(suffix).id,
3099
+ gitCommitHash: null,
3100
+ buildKey: suffix === "011" ? nativeBuildKey : null,
3101
+ artifact: {
3102
+ kind: "ipa",
3103
+ storageUri: `storage://read-budgets/native-builds/${suffix}/App.ipa`,
3104
+ byteSize: 1e3
3105
+ },
3106
+ embeddedBundle: {
3107
+ manifestStorageUri: `storage://bundles/${suffix}/manifest.json`,
3108
+ manifestFileHash: `manifest-hash-${suffix}`,
3109
+ assetBaseStorageUri: "storage://read-budgets/assets"
3110
+ }
3111
+ });
2984
3112
  const { apiKey } = await api.apiKeys.create({ name: "Read budgets" });
2985
3113
  for (let n = 1; n <= 24; n += 1) await api.insights.recordEvent(eventOf(n));
2986
3114
  await api.insights.recordEvent(eventOf(25, {
@@ -3208,6 +3336,66 @@ const READ_BUDGETS = [
3208
3336
  rows: 1
3209
3337
  }
3210
3338
  }),
3339
+ budget({
3340
+ api: "native build by ID: 1 point read",
3341
+ read: ({ core }) => core.getNativeBuild(createBundleFixture("8012").id),
3342
+ adapter: reads(1, 1, 0, 0),
3343
+ engine: {
3344
+ calls: 1,
3345
+ rows: 1
3346
+ },
3347
+ check: (build) => expect(build).toMatchObject({ min_bundle_id: createBundleFixture("012").id })
3348
+ }),
3349
+ budget({
3350
+ api: "native build by scope: byScope descending, limit 1",
3351
+ read: ({ core }) => core.findNativeBuild(nativeBuildScope),
3352
+ adapter: reads(0, 0, 1, 1),
3353
+ engine: {
3354
+ calls: 1,
3355
+ rows: 1
3356
+ },
3357
+ check: (build) => expect(build).toMatchObject({ min_bundle_id: createBundleFixture("012").id })
3358
+ }),
3359
+ budget({
3360
+ api: "native build by key: byBuildKey descending, limit 1",
3361
+ read: ({ core }) => core.findNativeBuild(nativeBuildScope, { buildKey: nativeBuildKey }),
3362
+ adapter: reads(0, 0, 1, 1),
3363
+ engine: {
3364
+ calls: 1,
3365
+ rows: 1
3366
+ },
3367
+ check: (build) => expect(build).toMatchObject({
3368
+ min_bundle_id: createBundleFixture("011").id,
3369
+ build_key: nativeBuildKey
3370
+ })
3371
+ }),
3372
+ budget({
3373
+ api: "native build list: all descending, limit rows",
3374
+ read: ({ core }) => core.listNativeBuilds({
3375
+ order: "desc",
3376
+ limit: 2
3377
+ }),
3378
+ adapter: reads(0, 0, 1, 2),
3379
+ engine: {
3380
+ calls: 1,
3381
+ rows: 2
3382
+ },
3383
+ check: (builds) => expect(builds.map(({ min_bundle_id }) => min_bundle_id)).toEqual(["012", "011"].map((suffix) => createBundleFixture(suffix).id))
3384
+ }),
3385
+ budget({
3386
+ api: "native build list of a channel: byChannel descending, limit rows",
3387
+ read: ({ core, productionId }) => core.listNativeBuilds({
3388
+ channelId: productionId,
3389
+ order: "desc",
3390
+ limit: 1
3391
+ }),
3392
+ adapter: reads(0, 0, 1, 1),
3393
+ engine: {
3394
+ calls: 1,
3395
+ rows: 1
3396
+ },
3397
+ check: (builds) => expect(builds).toMatchObject([{ min_bundle_id: createBundleFixture("012").id }])
3398
+ }),
3211
3399
  budget({
3212
3400
  api: "channel by name: 1 read",
3213
3401
  read: ({ core }) => core.findChannelByName("staging"),
@@ -3476,15 +3664,47 @@ const READ_BUDGETS = [
3476
3664
  writes: 1
3477
3665
  }),
3478
3666
  budget({
3479
- api: "channel delete: 1 read and 1 write",
3667
+ api: "channel delete: the channel, its native builds under it, and 1 write",
3480
3668
  read: ({ core, nightlyId }) => core.deleteChannel(nightlyId),
3669
+ adapter: reads(2, 2, 1, 0),
3670
+ engine: {
3671
+ calls: 3,
3672
+ rows: 2
3673
+ },
3674
+ writes: 1,
3675
+ check: (result) => expect(result).toEqual({ deleted: true })
3676
+ }),
3677
+ budget({
3678
+ api: "native build submission: the build and 1 write",
3679
+ read: ({ core }) => core.recordNativeBuildSubmission(createBundleFixture("8012").id, {
3680
+ destination: "testflight",
3681
+ version: "1.0.0 (12)"
3682
+ }),
3481
3683
  adapter: reads(1, 1, 0, 0),
3482
3684
  engine: {
3483
3685
  calls: 1,
3484
3686
  rows: 1
3485
3687
  },
3486
3688
  writes: 1,
3487
- check: (result) => expect(result).toEqual({ deleted: true })
3689
+ check: (build) => expect(build).toMatchObject({
3690
+ min_bundle_id: createBundleFixture("012").id,
3691
+ submitted_to: "testflight",
3692
+ submitted_version: "1.0.0 (12)"
3693
+ })
3694
+ }),
3695
+ budget({
3696
+ api: "native build delete: the build, its built-in bundle, and 1 write",
3697
+ read: ({ core }) => core.deleteNativeBuild(createBundleFixture("8011").id),
3698
+ adapter: reads(2, 2, 0, 0),
3699
+ engine: {
3700
+ calls: 2,
3701
+ rows: 2
3702
+ },
3703
+ writes: 1,
3704
+ check: (result) => expect(result).toMatchObject({
3705
+ deleted: true,
3706
+ nativeBuild: { min_bundle_id: createBundleFixture("011").id }
3707
+ })
3488
3708
  }),
3489
3709
  budget({
3490
3710
  api: "record an insights event: 2 dependent rounds of batch gets and 1 write",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/test-utils",
3
- "version": "1.1.4",
3
+ "version": "1.2.0-rc.1",
4
4
  "type": "module",
5
5
  "description": "Test utilities for Hot Updater packages",
6
6
  "sideEffects": false,
@@ -50,10 +50,10 @@
50
50
  "mysql2": "*",
51
51
  "pg": "*",
52
52
  "vitest": "^4.0.0",
53
- "@hot-updater/protocol": "^1.1.4",
54
- "@hot-updater/plugin-core": "^1.1.4",
55
- "@hot-updater/server": "^1.1.4",
56
- "@hot-updater/plugin-insights": "^1.1.4"
53
+ "@hot-updater/plugin-insights": "^1.2.0-rc.1",
54
+ "@hot-updater/protocol": "^1.2.0-rc.1",
55
+ "@hot-updater/plugin-core": "^1.2.0-rc.1",
56
+ "@hot-updater/server": "^1.2.0-rc.1"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@electric-sql/pglite": {
@@ -83,10 +83,10 @@
83
83
  "typescript": "npm:@typescript/typescript6@6.0.2",
84
84
  "vitest": "4.1.4",
85
85
  "workspace-tools": "^0.41.7",
86
- "@hot-updater/protocol": "1.1.4",
87
- "@hot-updater/plugin-core": "1.1.4",
88
- "@hot-updater/plugin-insights": "1.1.4",
89
- "@hot-updater/server": "1.1.4"
86
+ "@hot-updater/protocol": "1.2.0-rc.1",
87
+ "@hot-updater/plugin-core": "1.2.0-rc.1",
88
+ "@hot-updater/server": "1.2.0-rc.1",
89
+ "@hot-updater/plugin-insights": "1.2.0-rc.1"
90
90
  },
91
91
  "inlinedDependencies": {
92
92
  "@nodelib/fs.scandir": "2.1.5",