|
| 1 | +import fs from "fs"; |
| 2 | +import path from "path"; |
| 3 | +import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals"; |
| 4 | +import type { CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts"; |
| 5 | + |
| 6 | +/** |
| 7 | + * Checks the command definition against the entry it saves. Everything this command does |
| 8 | + * ends up in the release history the config is handed, so an option that never reaches it |
| 9 | + * leaves the release exactly as it was - which the command still reports as a success. |
| 10 | + */ |
| 11 | + |
| 12 | +const BINARY_VERSION = '1.0.0'; |
| 13 | +const APP_VERSION = '1.0.1'; |
| 14 | + |
| 15 | +let mockConfig: CliConfigInterface; |
| 16 | + |
| 17 | +jest.mock("../../utils/fsUtils.js", () => ({ |
| 18 | + findAndReadConfigFile: () => mockConfig, |
| 19 | +})); |
| 20 | + |
| 21 | +/** |
| 22 | + * Puts one released version in the config's history and records every history it is |
| 23 | + * handed back, so a case can read the entry as the consumer would store it. |
| 24 | + */ |
| 25 | +function stageReleaseHistory(): ReleaseHistoryInterface[] { |
| 26 | + const releaseHistory: ReleaseHistoryInterface = { |
| 27 | + [APP_VERSION]: { |
| 28 | + enabled: true, |
| 29 | + mandatory: false, |
| 30 | + downloadUrl: 'https://cdn.example.com/bundle', |
| 31 | + packageHash: 'a3f1c0', |
| 32 | + }, |
| 33 | + }; |
| 34 | + const saved: ReleaseHistoryInterface[] = []; |
| 35 | + |
| 36 | + mockConfig = { |
| 37 | + bundleUploader: async () => ({ downloadUrl: 'https://cdn.example.com/bundle' }), |
| 38 | + getReleaseHistory: async () => releaseHistory, |
| 39 | + // The command edits the history in place, so what it saved is copied out here. |
| 40 | + setReleaseHistory: async (_binaryVersion, _jsonFilePath, releaseInfo) => { |
| 41 | + saved.push(structuredClone(releaseInfo)); |
| 42 | + }, |
| 43 | + }; |
| 44 | + |
| 45 | + return saved; |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * Parses an `update-history` invocation against the real command definition. Commander is |
| 50 | + * asked to throw instead of exiting, and to keep its diagnostics to itself, so a rejected |
| 51 | + * option can be asserted on without ending the worker or the output. |
| 52 | + */ |
| 53 | +async function parseUpdateHistoryCommand(args: string[]): Promise<void> { |
| 54 | + const { program } = await import("commander"); |
| 55 | + await import("./index.js"); |
| 56 | + |
| 57 | + const updateHistoryCommand = program.commands.find((command) => command.name() === 'update-history'); |
| 58 | + updateHistoryCommand?.exitOverride(); |
| 59 | + updateHistoryCommand?.configureOutput({ writeErr: () => {} }); |
| 60 | + |
| 61 | + await program.parseAsync(['update-history', ...args], { from: 'user' }); |
| 62 | +} |
| 63 | + |
| 64 | +async function runUpdateHistoryCommand(args: string[]): Promise<void> { |
| 65 | + await parseUpdateHistoryCommand(['-b', BINARY_VERSION, '-v', APP_VERSION, ...args]); |
| 66 | +} |
| 67 | + |
| 68 | +let saved: ReleaseHistoryInterface[]; |
| 69 | + |
| 70 | +beforeEach(() => { |
| 71 | + jest.resetModules(); |
| 72 | + saved = stageReleaseHistory(); |
| 73 | + jest.spyOn(console, 'log').mockImplementation(() => {}); |
| 74 | + jest.spyOn(console, 'error').mockImplementation(() => {}); |
| 75 | + jest.spyOn(process, 'exit').mockImplementation(((code?: number) => { |
| 76 | + throw new Error(`process.exit(${code})`); |
| 77 | + }) as never); |
| 78 | +}); |
| 79 | + |
| 80 | +afterEach(() => { |
| 81 | + jest.restoreAllMocks(); |
| 82 | + // The command writes its JSON under the directory it was invoked in. |
| 83 | + fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true }); |
| 84 | +}); |
| 85 | + |
| 86 | +describe("update-history command options", () => { |
| 87 | + it("lowers the background wait of a release that is already out to zero seconds", async () => { |
| 88 | + await runUpdateHistoryCommand(['--minimum-background-duration', '0']); |
| 89 | + |
| 90 | + expect(saved).toHaveLength(1); |
| 91 | + expect(saved[0][APP_VERSION].minimumBackgroundDuration).toBe(0); |
| 92 | + }); |
| 93 | + |
| 94 | + it("leaves the entry saying nothing about the background wait when only --enable is given", async () => { |
| 95 | + await runUpdateHistoryCommand(['--enable', 'false']); |
| 96 | + |
| 97 | + expect(saved[0][APP_VERSION].enabled).toBe(false); |
| 98 | + expect(saved[0][APP_VERSION]).not.toHaveProperty('minimumBackgroundDuration'); |
| 99 | + }); |
| 100 | + |
| 101 | + it("saves the rollout percentage when --rollout is the only option given", async () => { |
| 102 | + await runUpdateHistoryCommand(['--rollout', '50']); |
| 103 | + |
| 104 | + expect(saved[0][APP_VERSION].rollout).toBe(50); |
| 105 | + }); |
| 106 | + |
| 107 | + it("exits without saving anything when no option says what to change", async () => { |
| 108 | + await expect(runUpdateHistoryCommand([])).rejects.toThrow('process.exit(1)'); |
| 109 | + |
| 110 | + expect(saved).toHaveLength(0); |
| 111 | + }); |
| 112 | + |
| 113 | + it("rejects a negative background wait and saves nothing", async () => { |
| 114 | + await expect(runUpdateHistoryCommand(['--minimum-background-duration', '-1'])) |
| 115 | + .rejects.toThrow('process.exit(1)'); |
| 116 | + |
| 117 | + expect(saved).toHaveLength(0); |
| 118 | + }); |
| 119 | + |
| 120 | + it.each([ |
| 121 | + ['a percentage above 100', '150'], |
| 122 | + ['a percentage that is not a number', 'abc'], |
| 123 | + ])("rejects %s and saves nothing", async (_scenario, rollout) => { |
| 124 | + await expect(runUpdateHistoryCommand(['--rollout', rollout])) |
| 125 | + .rejects.toThrow('process.exit(1)'); |
| 126 | + |
| 127 | + expect(saved).toHaveLength(0); |
| 128 | + }); |
| 129 | +}); |
0 commit comments