@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 +4 -2
- package/dist/index.mjs +225 -5
- package/package.json +9 -9
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,
|
|
68
|
-
* bundle's patch children. Each test deletes
|
|
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,
|
|
241
|
-
* bundle's patch children. Each test deletes
|
|
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,
|
|
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:
|
|
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: (
|
|
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
|
|
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/
|
|
54
|
-
"@hot-updater/
|
|
55
|
-
"@hot-updater/
|
|
56
|
-
"@hot-updater/
|
|
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
|
|
87
|
-
"@hot-updater/plugin-core": "1.1
|
|
88
|
-
"@hot-updater/
|
|
89
|
-
"@hot-updater/
|
|
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",
|