Verify the stable API and exact Capacitor iOS CORS contract before signing device-debug IPAs, with regression coverage and troubleshooting guidance.
496 lines
21 KiB
JavaScript
496 lines
21 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
DEBUG_API_URL,
|
|
DEBUG_BUNDLE_ID,
|
|
DEBUG_DISPLAY_NAME,
|
|
DEBUG_EXECUTABLE_NAME,
|
|
assertMinimumIosCompatible,
|
|
createRedactedLineWriter,
|
|
inspectIpa,
|
|
main,
|
|
parseArgs,
|
|
redactUdids,
|
|
requiresDeveloperMode,
|
|
selectIpaPayload,
|
|
selectUsbDevice,
|
|
spawnCommand,
|
|
validateDevelopmentArtifact,
|
|
validateManifest,
|
|
verifySha256File,
|
|
} from "../../scripts/mobile/ios-device.mjs";
|
|
|
|
const TEST_UDID = "00008110-0012345678901234";
|
|
const temporaryDirectories = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
|
|
});
|
|
|
|
function validArtifact(overrides = {}) {
|
|
const info = {
|
|
CFBundleIdentifier: DEBUG_BUNDLE_ID,
|
|
CFBundleDisplayName: DEBUG_DISPLAY_NAME,
|
|
CFBundleExecutable: DEBUG_EXECUTABLE_NAME,
|
|
CFBundleShortVersionString: "0.0.42",
|
|
CFBundleVersion: "4201",
|
|
MinimumOSVersion: "15.0",
|
|
...overrides.info,
|
|
};
|
|
const profile = {
|
|
ExpirationDate: "2027-01-01T00:00:00Z",
|
|
ProvisionedDevices: [TEST_UDID],
|
|
TeamIdentifier: ["TEAM123456"],
|
|
Entitlements: {
|
|
"application-identifier": `TEAM123456.${DEBUG_BUNDLE_ID}`,
|
|
"com.apple.developer.team-identifier": "TEAM123456",
|
|
"get-task-allow": true,
|
|
...overrides.entitlements,
|
|
},
|
|
...overrides.profile,
|
|
};
|
|
return { info, profile };
|
|
}
|
|
|
|
function installedApp(overrides = {}) {
|
|
return {
|
|
CFBundleIdentifier: DEBUG_BUNDLE_ID,
|
|
CFBundleExecutable: DEBUG_EXECUTABLE_NAME,
|
|
CFBundleShortVersionString: "0.0.42",
|
|
CFBundleVersion: "4201",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function commandResult(stdout = "", stderr = "", code = 0) {
|
|
return { code, stdout: Buffer.from(stdout), stderr: Buffer.from(stderr) };
|
|
}
|
|
|
|
function createHarness({
|
|
developerMode = "enabled",
|
|
installedSequence = [],
|
|
installationProxyError,
|
|
pairingError,
|
|
productVersion = "27.0",
|
|
minimumIos = "15.0",
|
|
} = {}) {
|
|
const calls = [];
|
|
const output = [];
|
|
const errors = [];
|
|
const installed = [...installedSequence];
|
|
const run = async (command, args, options = {}) => {
|
|
calls.push({ command, args, options });
|
|
if (command === "idevice_id") return commandResult(`${TEST_UDID}\n`);
|
|
if (command === "idevicepair") {
|
|
return pairingError ? commandResult("", pairingError, 1) : commandResult("SUCCESS");
|
|
}
|
|
if (command === "idevicedevmodectl") {
|
|
return commandResult(`Device DeveloperMode\n${TEST_UDID} ${developerMode}\n`);
|
|
}
|
|
if (command === "ideviceinfo") {
|
|
const key = args.at(-1);
|
|
const values = {
|
|
ActivationState: "Activated",
|
|
ProductType: "iPhone15,4",
|
|
ProductVersion: productVersion,
|
|
};
|
|
return commandResult(values[key] ?? "");
|
|
}
|
|
if (command === "ideviceinstaller") {
|
|
if (installationProxyError && args.includes("list")) {
|
|
return commandResult("", installationProxyError, 1);
|
|
}
|
|
return commandResult();
|
|
}
|
|
if (command === "idevicesyslog" || command === "idevicecrashreport") {
|
|
return commandResult();
|
|
}
|
|
throw new Error(`Unexpected command: ${command}`);
|
|
};
|
|
return {
|
|
calls,
|
|
output,
|
|
errors,
|
|
overrides: {
|
|
run,
|
|
commandExists: async () => true,
|
|
inspectIpa: async (_context, ipaPath) => ({
|
|
ipaPath,
|
|
validated: {
|
|
executable: DEBUG_EXECUTABLE_NAME,
|
|
version: "0.0.42",
|
|
build: "4201",
|
|
minimumIos,
|
|
},
|
|
}),
|
|
getInstalledDebugApp: async () => installed.shift() ?? null,
|
|
now: () => new Date("2026-07-20T00:00:00Z"),
|
|
out: (message) => output.push(message),
|
|
err: (message) => errors.push(message),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("argument and device selection safety", () => {
|
|
it("accepts the documented command shapes", () => {
|
|
expect(parseArgs(["doctor", "--udid", TEST_UDID])).toMatchObject({
|
|
command: "doctor",
|
|
udid: TEST_UDID,
|
|
});
|
|
expect(parseArgs(["install", "build.ipa", "--manifest", "manifest.json"])).toMatchObject({
|
|
command: "install",
|
|
manifest: "manifest.json",
|
|
positionals: ["build.ipa"],
|
|
});
|
|
});
|
|
|
|
it("requires the exact debug identifier for uninstall", () => {
|
|
expect(() => parseArgs(["uninstall", "--confirm", "io.truckwash.app"])).toThrow("Refusing to uninstall");
|
|
expect(parseArgs(["uninstall", "--confirm", DEBUG_BUNDLE_ID]).confirm).toBe(DEBUG_BUNDLE_ID);
|
|
});
|
|
|
|
it("requires exactly one USB device by default", () => {
|
|
expect(() => selectUsbDevice("", undefined)).toThrow("No cable-connected iPhone");
|
|
expect(() => selectUsbDevice("device-a\ndevice-b\n", undefined)).toThrow("Select one explicitly");
|
|
expect(selectUsbDevice("device-a\ndevice-b\n", "device-b").udid).toBe("device-b");
|
|
});
|
|
|
|
it("redacts both modern and legacy UDIDs", () => {
|
|
const legacy = "a".repeat(40);
|
|
expect(redactUdids(`device ${TEST_UDID} and ${legacy}`)).toBe("device <redacted-udid> and <redacted-udid>");
|
|
});
|
|
|
|
it("redacts a UDID split across streamed child-process chunks", () => {
|
|
let output = "";
|
|
const writer = createRedactedLineWriter({ write: (value) => (output += value) }, [TEST_UDID]);
|
|
writer.write(Buffer.from(`connected: ${TEST_UDID.slice(0, 10)}`));
|
|
writer.write(Buffer.from(`${TEST_UDID.slice(10)}\nready\n`));
|
|
writer.end();
|
|
expect(output).toBe("connected: <redacted-udid>\nready\n");
|
|
});
|
|
|
|
it("writes real child-process stdout to a file only after UDID redaction", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "mobile-ios-redacted-log-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const outputPath = join(directory, "device.log");
|
|
const script = `process.stdout.write(${JSON.stringify(`connected:${TEST_UDID}\nmessage\n`)})`;
|
|
|
|
await expect(
|
|
spawnCommand(process.execPath, ["-e", script], {
|
|
streamRedactedUdids: [TEST_UDID],
|
|
redactedStdoutFile: outputPath,
|
|
})
|
|
).resolves.toMatchObject({ code: 0 });
|
|
expect(await readFile(outputPath, "utf8")).toBe("connected:<redacted-udid>\nmessage\n");
|
|
expect((await stat(outputPath)).mode & 0o777).toBe(0o600);
|
|
});
|
|
|
|
it("rejects an invalid redacted output path before starting the child", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "mobile-ios-invalid-log-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const outputPath = join(directory, "missing", "device.log");
|
|
|
|
await expect(
|
|
spawnCommand(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
|
|
streamRedactedUdids: [TEST_UDID],
|
|
redactedStdoutFile: outputPath,
|
|
})
|
|
).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
});
|
|
|
|
describe("IPA validation", () => {
|
|
it.each([
|
|
["equal versions", "17.0", "17"],
|
|
["newer connected iOS", "15.2.1", "17.0"],
|
|
["prerelease suffixes", "17.0-beta.1", "17.0rc2"],
|
|
])("accepts compatible minimum iOS for %s", (_name, minimumIos, deviceIos) => {
|
|
expect(() => assertMinimumIosCompatible(minimumIos, deviceIos)).not.toThrow();
|
|
});
|
|
|
|
it("rejects an IPA requiring newer iOS", () => {
|
|
expect(() => assertMinimumIosCompatible("18.0", "17.6.1")).toThrow("IPA requires iOS 18.0");
|
|
});
|
|
|
|
it.each([
|
|
["malformed minimum", "17..0", "17.0", "IPA minimum iOS version is malformed"],
|
|
["malformed device version", "17.0", "version-17", "Connected iPhone iOS version is malformed"],
|
|
["missing minimum", "", "17.0", "IPA minimum iOS version is malformed"],
|
|
])("rejects %s", (_name, minimumIos, deviceIos, message) => {
|
|
expect(() => assertMinimumIosCompatible(minimumIos, deviceIos)).toThrow(message);
|
|
});
|
|
|
|
it("accepts a valid development artifact", () => {
|
|
expect(validateDevelopmentArtifact(validArtifact(), TEST_UDID, new Date("2026-07-20T00:00:00Z"))).toMatchObject({
|
|
bundleId: DEBUG_BUNDLE_ID,
|
|
executable: DEBUG_EXECUTABLE_NAME,
|
|
version: "0.0.42",
|
|
build: "4201",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["production bundle", { info: { CFBundleIdentifier: "io.truckwash.app" } }, "expected io.truckwash.app.debug"],
|
|
["production executable", { info: { CFBundleExecutable: "App" } }, `expected ${DEBUG_EXECUTABLE_NAME}`],
|
|
["App Store profile", { entitlements: { "get-task-allow": false } }, "get-task-allow"],
|
|
[
|
|
"ad-hoc profile",
|
|
{ entitlements: { "get-task-allow": false }, profile: { ProvisionedDevices: [TEST_UDID] } },
|
|
"get-task-allow",
|
|
],
|
|
["missing device", { profile: { ProvisionedDevices: ["another-device"] } }, "not included"],
|
|
["expired profile", { profile: { ExpirationDate: "2026-01-01T00:00:00Z" } }, "expired"],
|
|
[
|
|
"wrong application identifier",
|
|
{ entitlements: { "application-identifier": "TEAM123456.io.other.app" } },
|
|
"application identifier",
|
|
],
|
|
["wrong team identifier", { profile: { TeamIdentifier: ["OTHERTEAM"] } }, "entitlement team identifier"],
|
|
])("rejects a %s", (_name, overrides, message) => {
|
|
expect(() =>
|
|
validateDevelopmentArtifact(validArtifact(overrides), TEST_UDID, new Date("2026-07-20T00:00:00Z"))
|
|
).toThrow(message);
|
|
});
|
|
|
|
it("requires exactly one top-level app payload and its profile", () => {
|
|
expect(selectIpaPayload(["Payload/App.app/Info.plist", "Payload/App.app/embedded.mobileprovision"])).toMatchObject({
|
|
appRoot: "Payload/App.app",
|
|
});
|
|
expect(() =>
|
|
selectIpaPayload([
|
|
"Payload/App.app/Info.plist",
|
|
"Payload/Other.app/Info.plist",
|
|
"Payload/App.app/embedded.mobileprovision",
|
|
])
|
|
).toThrow("exactly one app payload");
|
|
expect(() => selectIpaPayload(["Payload/App.app/Info.plist"])).toThrow("embedded provisioning profile");
|
|
});
|
|
|
|
it("compares manifest identity and version fields with the IPA", () => {
|
|
const artifact = validArtifact();
|
|
expect(() =>
|
|
validateManifest(
|
|
{
|
|
schema_version: 1,
|
|
signing_method: "development",
|
|
repository: "copenhagentruckwash/pleno-vue",
|
|
source_ref: "agent/ios-device-debug",
|
|
source_sha: "a".repeat(40),
|
|
api_url: DEBUG_API_URL,
|
|
release_manager_control_api_url: DEBUG_API_URL,
|
|
bundle_id: DEBUG_BUNDLE_ID,
|
|
display_name: DEBUG_DISPLAY_NAME,
|
|
executable_name: DEBUG_EXECUTABLE_NAME,
|
|
version: "0.0.42",
|
|
build: "4201",
|
|
minimum_ios: "15.0",
|
|
profile_expiration_utc: "2027-01-01T00:00:00Z",
|
|
ipa_filename: "debug.ipa",
|
|
},
|
|
artifact,
|
|
"/tmp/debug.ipa"
|
|
)
|
|
).not.toThrow();
|
|
expect(() =>
|
|
validateManifest({ schema_version: 1, signing_method: "development" }, artifact, "/tmp/debug.ipa")
|
|
).toThrow("repository");
|
|
expect(() =>
|
|
validateManifest({ schema_version: 1, signing_method: "app-store" }, artifact, "/tmp/debug.ipa")
|
|
).toThrow("signing method");
|
|
});
|
|
|
|
it("requires Developer Mode only on iOS 16 and newer", () => {
|
|
expect(requiresDeveloperMode("15.8.4")).toBe(false);
|
|
expect(requiresDeveloperMode("16.0")).toBe(true);
|
|
expect(requiresDeveloperMode("27.0 beta")).toBe(true);
|
|
});
|
|
|
|
it("verifies SHA256SUMS and rejects a mismatch", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "mobile-ios-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const ipaPath = join(directory, "debug.ipa");
|
|
const checksumPath = join(directory, "SHA256SUMS");
|
|
const content = Buffer.from("fake IPA fixture");
|
|
await writeFile(ipaPath, content);
|
|
const digest = createHash("sha256").update(content).digest("hex");
|
|
await writeFile(checksumPath, `${digest} debug.ipa\n`);
|
|
await expect(verifySha256File(ipaPath, checksumPath)).resolves.toBeUndefined();
|
|
await writeFile(checksumPath, `${"0".repeat(64)} debug.ipa\n`);
|
|
await expect(verifySha256File(ipaPath, checksumPath)).rejects.toThrow("Checksum verification failed");
|
|
expect(await readFile(ipaPath, "utf8")).toBe("fake IPA fixture");
|
|
});
|
|
|
|
it("requires the workflow manifest and checksums before inspecting an IPA", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "mobile-ios-artifact-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const ipaPath = join(directory, "debug.ipa");
|
|
await writeFile(ipaPath, "fixture");
|
|
const context = { commandExists: async () => true };
|
|
|
|
await expect(inspectIpa(context, ipaPath, { connectedUdid: TEST_UDID })).rejects.toThrow("Manifest file not found");
|
|
await writeFile(join(directory, "manifest.json"), "{}");
|
|
await expect(inspectIpa(context, ipaPath, { connectedUdid: TEST_UDID })).rejects.toThrow("Checksum file not found");
|
|
});
|
|
});
|
|
|
|
describe("device commands", () => {
|
|
it("doctor checks pairing, activation, installation access, and Developer Mode", async () => {
|
|
const harness = createHarness();
|
|
await expect(main(["doctor"], harness.overrides)).resolves.toBe(0);
|
|
expect(harness.calls.map(({ command }) => command)).toEqual(
|
|
expect.arrayContaining(["idevice_id", "idevicepair", "ideviceinfo", "ideviceinstaller", "idevicedevmodectl"])
|
|
);
|
|
expect(harness.output.join("\n")).not.toContain(TEST_UDID);
|
|
expect(harness.output.join("\n")).toContain("Developer Mode: enabled");
|
|
});
|
|
|
|
it("doctor gives targeted guidance when Developer Mode is disabled", async () => {
|
|
const harness = createHarness({ developerMode: "disabled" });
|
|
await expect(main(["doctor"], harness.overrides)).resolves.toBe(1);
|
|
expect(harness.errors.join("\n")).toContain("Developer Mode is disabled");
|
|
expect(harness.errors.join("\n")).not.toContain(TEST_UDID);
|
|
});
|
|
|
|
it("doctor accepts an iOS 15 device without invoking Developer Mode tooling", async () => {
|
|
const harness = createHarness({ developerMode: "disabled", productVersion: "15.8.4" });
|
|
await expect(main(["doctor"], harness.overrides)).resolves.toBe(0);
|
|
expect(harness.calls.some(({ command }) => command === "idevicedevmodectl")).toBe(false);
|
|
expect(harness.output.join("\n")).toContain("not required before iOS 16");
|
|
});
|
|
|
|
it("doctor gives targeted guidance when the phone is locked", async () => {
|
|
const harness = createHarness({
|
|
installationProxyError: "Could not connect: device is password protected and locked",
|
|
});
|
|
await expect(main(["doctor"], harness.overrides)).resolves.toBe(1);
|
|
expect(harness.errors.join("\n")).toContain("iPhone is locked");
|
|
expect(harness.errors.join("\n")).not.toContain(TEST_UDID);
|
|
});
|
|
|
|
it("doctor gives targeted guidance when trust is invalid", async () => {
|
|
const harness = createHarness({ pairingError: "ERROR: Invalid HostID / pairing record" });
|
|
await expect(main(["doctor"], harness.overrides)).resolves.toBe(1);
|
|
expect(harness.errors.join("\n")).toContain("Pairing is not valid");
|
|
expect(harness.errors.join("\n")).not.toContain(TEST_UDID);
|
|
});
|
|
|
|
it.each([
|
|
["install", null],
|
|
["upgrade", installedApp()],
|
|
])("chooses %s and verifies the installed version", async (action, initialApp) => {
|
|
const harness = createHarness({
|
|
installedSequence: [initialApp, installedApp()],
|
|
});
|
|
await expect(main(["install", "/tmp/debug.ipa"], harness.overrides)).resolves.toBe(0);
|
|
expect(
|
|
harness.calls.some(
|
|
({ command, args }) =>
|
|
command === "ideviceinstaller" && args.includes(action) && args.includes("/tmp/debug.ipa")
|
|
)
|
|
).toBe(true);
|
|
expect(harness.output.at(-1)).toContain("0.0.42 (4201)");
|
|
});
|
|
|
|
it("rejects an incompatible device before any install or upgrade mutation", async () => {
|
|
const harness = createHarness({ productVersion: "14.8.1", minimumIos: "15.0" });
|
|
await expect(main(["install", "/tmp/debug.ipa"], harness.overrides)).resolves.toBe(1);
|
|
expect(harness.errors.join("\n")).toContain("IPA requires iOS 15.0");
|
|
const mutatingCalls = harness.calls.filter(
|
|
({ command, args }) =>
|
|
command === "ideviceinstaller" &&
|
|
(args.includes("install") || args.includes("upgrade") || args.includes("uninstall"))
|
|
);
|
|
expect(mutatingCalls).toHaveLength(0);
|
|
});
|
|
|
|
it("rejects a post-install version mismatch", async () => {
|
|
const harness = createHarness({
|
|
installedSequence: [null, installedApp({ CFBundleVersion: "wrong" })],
|
|
});
|
|
await expect(main(["install", "/tmp/debug.ipa"], harness.overrides)).resolves.toBe(1);
|
|
expect(harness.errors.join("\n")).toContain("Installed version verification failed");
|
|
});
|
|
|
|
it("filters logs by the exact installed executable", async () => {
|
|
const harness = createHarness({ installedSequence: [installedApp()] });
|
|
await expect(main(["logs", "--output", "device.log"], harness.overrides)).resolves.toBe(0);
|
|
const call = harness.calls.find(({ command }) => command === "idevicesyslog");
|
|
expect(call.args).toEqual(expect.arrayContaining(["-p", DEBUG_EXECUTABLE_NAME]));
|
|
expect(call.args).not.toContain("--output");
|
|
expect(call.options).toMatchObject({
|
|
streamRedactedUdids: [TEST_UDID],
|
|
redactedStdoutFile: expect.stringMatching(/device\.log$/u),
|
|
});
|
|
});
|
|
|
|
it("copies and extracts only debug-app crashes while keeping them on-device", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "mobile-ios-crashes-test-"));
|
|
temporaryDirectories.push(directory);
|
|
const harness = createHarness({ installedSequence: [installedApp({ CFBundleExecutable: DEBUG_EXECUTABLE_NAME })] });
|
|
await expect(main(["crashes", directory], harness.overrides)).resolves.toBe(0);
|
|
const call = harness.calls.find(({ command }) => command === "idevicecrashreport");
|
|
expect(call.args).toEqual(
|
|
expect.arrayContaining(["--keep", "--extract", "--filter", DEBUG_EXECUTABLE_NAME, directory])
|
|
);
|
|
expect(call.args).not.toContain("--remove-all");
|
|
});
|
|
|
|
it("refuses log collection from an older debug build with the production executable name", async () => {
|
|
const harness = createHarness({ installedSequence: [installedApp({ CFBundleExecutable: "App" })] });
|
|
await expect(main(["logs"], harness.overrides)).resolves.toBe(1);
|
|
expect(harness.errors.join("\n")).toContain(`expected ${DEBUG_EXECUTABLE_NAME}`);
|
|
expect(harness.calls.some(({ command }) => command === "idevicesyslog")).toBe(false);
|
|
});
|
|
|
|
it("uninstalls only the debug bundle after exact confirmation", async () => {
|
|
const harness = createHarness({ installedSequence: [installedApp(), null] });
|
|
await expect(main(["uninstall", "--confirm", DEBUG_BUNDLE_ID], harness.overrides)).resolves.toBe(0);
|
|
const call = harness.calls.find(
|
|
({ command, args }) => command === "ideviceinstaller" && args.includes("uninstall")
|
|
);
|
|
expect(call.args.at(-1)).toBe(DEBUG_BUNDLE_ID);
|
|
expect(call.args).not.toContain("io.truckwash.app");
|
|
});
|
|
|
|
it("does not touch the device when uninstall confirmation is wrong", async () => {
|
|
const harness = createHarness();
|
|
await expect(main(["uninstall", "--confirm", "io.truckwash.app"], harness.overrides)).resolves.toBe(1);
|
|
expect(harness.calls).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("repository device-debug configuration", () => {
|
|
it("keeps Debug and Release as distinct installable apps", async () => {
|
|
const project = await readFile("ios/App/App.xcodeproj/project.pbxproj", "utf8");
|
|
const debugTarget = project.match(/504EC3171FED79650016851F \/\* Debug \*\/ = \{[\s\S]*?\n\t\t\};/u)?.[0];
|
|
const releaseTarget = project.match(/504EC3181FED79650016851F \/\* Release \*\/ = \{[\s\S]*?\n\t\t\};/u)?.[0];
|
|
const infoPlist = await readFile("ios/App/App/Info.plist", "utf8");
|
|
|
|
expect(debugTarget).toContain('APP_DISPLAY_NAME = "Truck Wash Debug";');
|
|
expect(debugTarget).toContain(`PRODUCT_BUNDLE_IDENTIFIER = ${DEBUG_BUNDLE_ID};`);
|
|
expect(debugTarget).toContain(`PRODUCT_NAME = ${DEBUG_EXECUTABLE_NAME};`);
|
|
expect(releaseTarget).toContain('APP_DISPLAY_NAME = "Truck Wash";');
|
|
expect(releaseTarget).toContain("PRODUCT_BUNDLE_IDENTIFIER = io.truckwash.app;");
|
|
expect(infoPlist).toContain("<string>$(APP_DISPLAY_NAME)</string>");
|
|
});
|
|
|
|
it("keeps signing secrets behind the master-only environment workflow", async () => {
|
|
const workflow = await readFile(".github/workflows/ios-device-debug.yml", "utf8");
|
|
|
|
expect(workflow).toContain('if [[ "$WORKFLOW_REF" != "refs/heads/master" ]]');
|
|
expect(workflow).toContain("name: mobile-device-debug");
|
|
expect(workflow).toContain("RELEASE_COMMIT_SHA: ${{ env.RESOLVED_SOURCE_SHA }}");
|
|
expect(workflow).toContain("IOS_DEBUG_CERTIFICATE_BASE64: ${{ secrets.IOS_DEBUG_CERTIFICATE_BASE64 }}");
|
|
expect(workflow).toContain("Origin: capacitor://localhost");
|
|
expect(workflow).toContain("Stable API did not allow the exact capacitor://localhost origin");
|
|
expect(workflow).toContain("Stable API did not allow credentialed Capacitor requests");
|
|
expect(workflow).toContain("Stable API did not allow the Authorization header from the Capacitor origin");
|
|
expect(workflow).not.toContain("IOS_DEBUG_KEYCHAIN_PASSWORD");
|
|
expect(workflow).not.toMatch(/upload-app|notarytool|transporter/iu);
|
|
});
|
|
});
|