Skip to content

Commit e8cebec

Browse files
committed
refactor(deploy): route lifecycle test failures through api mock
1 parent 6dda179 commit e8cebec

5 files changed

Lines changed: 194 additions & 49 deletions

File tree

packages/cli-core/src/commands/deploy/api.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mock.module("../../lib/sleep.ts", () => ({
2424
const deployApiModulePath = "./api.ts?adapter-test";
2525
const {
2626
createProductionInstance,
27+
configureMockDeployApi,
2728
getDeployStatus,
2829
patchInstanceConfig,
2930
validateCloning,
@@ -78,4 +79,45 @@ describe("deploy api adapter", () => {
7879
expect(await getDeployStatus("app_123", "ins_prod_123")).toEqual({ status: "complete" });
7980
expect(mockPlapiGetDeployStatus).not.toHaveBeenCalled();
8081
});
82+
83+
test("mock deploy api can fail lifecycle operations with PLAPI-shaped errors", async () => {
84+
configureMockDeployApi({
85+
failValidateCloning: true,
86+
failCreateProductionInstance: true,
87+
failDnsVerification: true,
88+
failOAuthSave: true,
89+
});
90+
91+
await expect(validateCloning("app_123", { clone_instance_id: "ins_dev_123" })).rejects.toThrow(
92+
"Simulated deploy failure: cloning validation.",
93+
);
94+
await expect(
95+
createProductionInstance("app_123", {
96+
home_url: "example.com",
97+
clone_instance_id: "ins_dev_123",
98+
}),
99+
).rejects.toThrow("Simulated deploy failure: production instance creation.");
100+
await expect(getDeployStatus("app_123", "ins_prod_123")).rejects.toThrow(
101+
"Simulated deploy failure: DNS verification.",
102+
);
103+
await expect(
104+
patchInstanceConfig("app_123", "ins_prod_123", {
105+
connection_oauth_google: { enabled: true },
106+
}),
107+
).rejects.toThrow("Simulated deploy failure: OAuth credential save.");
108+
109+
expect(mockPlapiValidateCloning).not.toHaveBeenCalled();
110+
expect(mockPlapiCreateProductionInstance).not.toHaveBeenCalled();
111+
expect(mockPlapiGetDeployStatus).not.toHaveBeenCalled();
112+
expect(mockPlapiPatchInstanceConfig).not.toHaveBeenCalled();
113+
});
114+
115+
test("reset mock deploy api clears lifecycle failure flags", async () => {
116+
configureMockDeployApi({ failValidateCloning: true });
117+
_resetDeployStatusMock();
118+
119+
await expect(
120+
validateCloning("app_123", { clone_instance_id: "ins_dev_123" }),
121+
).resolves.toBeUndefined();
122+
});
81123
});

packages/cli-core/src/commands/deploy/api.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
import { sleep } from "../../lib/sleep.ts";
12+
import { PlapiError } from "../../lib/errors.ts";
1213
import {
1314
createProductionInstance as liveCreateProductionInstance,
1415
getDeployStatus as liveGetDeployStatus,
@@ -54,6 +55,27 @@ const MOCK_SECRET_KEY = "MOCKED_NOT_REAL_FIXME";
5455
const MOCK_LATENCY_MS = 2000;
5556
const MOCK_INCOMPLETE_POLLS = 2;
5657

58+
type DeployApiMockOptions = {
59+
failValidateCloning?: boolean;
60+
failCreateProductionInstance?: boolean;
61+
failDnsVerification?: boolean;
62+
failOAuthSave?: boolean;
63+
};
64+
65+
let mockOptions: DeployApiMockOptions = {};
66+
67+
export function configureMockDeployApi(options: DeployApiMockOptions = {}): void {
68+
mockOptions = { ...options };
69+
}
70+
71+
function simulatedDeployApiFailure(step: string): PlapiError {
72+
return new PlapiError(
73+
500,
74+
JSON.stringify({ errors: [{ message: `Simulated deploy failure: ${step}.` }] }),
75+
"clerk deploy test flag",
76+
);
77+
}
78+
5779
async function simulateServerLatency(): Promise<void> {
5880
await sleep(MOCK_LATENCY_MS);
5981
}
@@ -74,11 +96,15 @@ const deployStatusPollCounts = new Map<string, number>();
7496

7597
export function _resetDeployStatusMock(): void {
7698
deployStatusPollCounts.clear();
99+
configureMockDeployApi();
77100
}
78101

79102
export const mockDeployApi: DeployApi = {
80103
async createProductionInstance(_applicationId, params) {
81104
await simulateServerLatency();
105+
if (mockOptions.failCreateProductionInstance) {
106+
throw simulatedDeployApiFailure("production instance creation");
107+
}
82108
return {
83109
instance_id: MOCK_PRODUCTION_INSTANCE_ID,
84110
environment_type: "production",
@@ -94,10 +120,16 @@ export const mockDeployApi: DeployApi = {
94120

95121
async validateCloning() {
96122
await simulateServerLatency();
123+
if (mockOptions.failValidateCloning) {
124+
throw simulatedDeployApiFailure("cloning validation");
125+
}
97126
},
98127

99128
async getDeployStatus(applicationId, envOrInsId) {
100129
await simulateServerLatency();
130+
if (mockOptions.failDnsVerification) {
131+
throw simulatedDeployApiFailure("DNS verification");
132+
}
101133
const key = `${applicationId}:${envOrInsId}`;
102134
const count = (deployStatusPollCounts.get(key) ?? 0) + 1;
103135
deployStatusPollCounts.set(key, count);
@@ -116,6 +148,9 @@ export const mockDeployApi: DeployApi = {
116148

117149
async patchInstanceConfig() {
118150
await simulateServerLatency();
151+
if (mockOptions.failOAuthSave) {
152+
throw simulatedDeployApiFailure("OAuth credential save");
153+
}
119154
return {};
120155
},
121156
};

packages/cli-core/src/commands/deploy/index.test.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,27 @@ const mockValidateCloning = mock();
3232
const mockGetDeployStatus = mock();
3333
const mockRetrySSL = mock();
3434
const mockRetryMail = mock();
35+
const mockConfigureMockDeployApi = mock();
3536
const mockDomainConnectUrl = mock();
3637

38+
type DeployApiMockOptions = {
39+
failValidateCloning?: boolean;
40+
failCreateProductionInstance?: boolean;
41+
failDnsVerification?: boolean;
42+
failOAuthSave?: boolean;
43+
};
44+
45+
let mockDeployApiOptions: DeployApiMockOptions = {};
46+
47+
function configureMockDeployApi(options: DeployApiMockOptions = {}) {
48+
mockConfigureMockDeployApi(options);
49+
mockDeployApiOptions = { ...options };
50+
}
51+
52+
function simulatedDeployApiFailure(step: string): Error {
53+
return new Error(`Simulated deploy failure: ${step}.`);
54+
}
55+
3756
mock.module("@inquirer/prompts", () => ({
3857
...promptsStubs,
3958
select: (...args: unknown[]) => mockSelect(...args),
@@ -55,15 +74,46 @@ mock.module("../../lib/plapi.ts", () => ({
5574
fetchInstanceConfig: (...args: unknown[]) => mockFetchInstanceConfig(...args),
5675
fetchApplication: (...args: unknown[]) => mockFetchApplication(...args),
5776
listApplicationDomains: (...args: unknown[]) => mockListApplicationDomains(...args),
58-
}));
59-
60-
mock.module("./api.ts", () => ({
6177
createProductionInstance: (...args: unknown[]) => mockCreateProductionInstance(...args),
6278
validateCloning: (...args: unknown[]) => mockValidateCloning(...args),
6379
getDeployStatus: (...args: unknown[]) => mockGetDeployStatus(...args),
80+
patchInstanceConfig: (...args: unknown[]) => mockPatchInstanceConfig(...args),
6481
retryApplicationDomainSSL: (...args: unknown[]) => mockRetrySSL(...args),
6582
retryApplicationDomainMail: (...args: unknown[]) => mockRetryMail(...args),
66-
patchInstanceConfig: (...args: unknown[]) => mockPatchInstanceConfig(...args),
83+
}));
84+
85+
mock.module("./api.ts", () => ({
86+
configureMockDeployApi,
87+
createProductionInstance: (...args: unknown[]) => {
88+
const result = mockCreateProductionInstance(...args);
89+
if (mockDeployApiOptions.failCreateProductionInstance) {
90+
throw simulatedDeployApiFailure("production instance creation");
91+
}
92+
return result;
93+
},
94+
validateCloning: (...args: unknown[]) => {
95+
const result = mockValidateCloning(...args);
96+
if (mockDeployApiOptions.failValidateCloning) {
97+
throw simulatedDeployApiFailure("cloning validation");
98+
}
99+
return result;
100+
},
101+
getDeployStatus: (...args: unknown[]) => {
102+
const result = mockGetDeployStatus(...args);
103+
if (mockDeployApiOptions.failDnsVerification) {
104+
throw simulatedDeployApiFailure("DNS verification");
105+
}
106+
return result;
107+
},
108+
retryApplicationDomainSSL: (...args: unknown[]) => mockRetrySSL(...args),
109+
retryApplicationDomainMail: (...args: unknown[]) => mockRetryMail(...args),
110+
patchInstanceConfig: (...args: unknown[]) => {
111+
const result = mockPatchInstanceConfig(...args);
112+
if (mockDeployApiOptions.failOAuthSave) {
113+
throw simulatedDeployApiFailure("OAuth credential save");
114+
}
115+
return result;
116+
},
67117
}));
68118

69119
mock.module("./domain-connect.ts", () => ({
@@ -188,6 +238,8 @@ describe("deploy", () => {
188238
mockGetDeployStatus.mockReset();
189239
mockRetrySSL.mockReset();
190240
mockRetryMail.mockReset();
241+
mockConfigureMockDeployApi.mockReset();
242+
mockDeployApiOptions = {};
191243
mockDomainConnectUrl.mockReset();
192244
consoleSpy?.mockRestore();
193245
stderrSpy?.mockRestore();
@@ -900,6 +952,9 @@ describe("deploy", () => {
900952
expect(mockValidateCloning).toHaveBeenCalledWith("app_xyz789", {
901953
clone_instance_id: "ins_dev_123",
902954
});
955+
expect(mockConfigureMockDeployApi).toHaveBeenCalledWith(
956+
expect.objectContaining({ failValidateCloning: true }),
957+
);
903958
expect(mockCreateProductionInstance).not.toHaveBeenCalled();
904959
});
905960

@@ -918,6 +973,9 @@ describe("deploy", () => {
918973
home_url: "example.com",
919974
clone_instance_id: "ins_dev_123",
920975
});
976+
expect(mockConfigureMockDeployApi).toHaveBeenCalledWith(
977+
expect.objectContaining({ failCreateProductionInstance: true }),
978+
);
921979
});
922980

923981
test("--test-fail-dns-verification simulates DNS verification failure", async () => {
@@ -941,6 +999,9 @@ describe("deploy", () => {
941999
);
9421000

9431001
expect(mockGetDeployStatus).toHaveBeenCalledWith("app_xyz789", "ins_prod_mock");
1002+
expect(mockConfigureMockDeployApi).toHaveBeenCalledWith(
1003+
expect.objectContaining({ failDnsVerification: true }),
1004+
);
9441005
expect(mockPatchInstanceConfig).not.toHaveBeenCalled();
9451006
});
9461007

@@ -970,6 +1031,9 @@ describe("deploy", () => {
9701031
client_secret: "google-secret",
9711032
},
9721033
});
1034+
expect(mockConfigureMockDeployApi).toHaveBeenCalledWith(
1035+
expect.objectContaining({ failOAuthSave: true }),
1036+
);
9731037
});
9741038

9751039
test("plain deploy resumes DNS verification from live API state", async () => {

0 commit comments

Comments
 (0)