Skip to content

Commit cd17369

Browse files
JacobMGEvanspetebacondarwin
authored andcommitted
non-TTY check for required variables:
Added a check in non-TTY environments for `account_id`, `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`. If `account_id` exists in `wrangler.toml` then `CLOUDFLARE_ACCOUNT_ID` is not needed in non-TTY scope. The `CLOUDFLARE_API_TOKEN` is necessary in non-TTY scope and will always error if missing. resolves #827
1 parent 97f945f commit cd17369

File tree

10 files changed

+232
-49
lines changed

10 files changed

+232
-49
lines changed

.changeset/eighty-yaks-jump.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
feat: non-TTY check for required variables
6+
Added a check in non-TTY environments for `account_id`, `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`. If `account_id` exists in `wrangler.toml`
7+
then `CLOUDFLARE_ACCOUNT_ID` is not needed in non-TTY scope. The `CLOUDFLARE_API_TOKEN` is necessary in non-TTY scope and will always error if missing.
8+
9+
resolves #827

packages/wrangler/src/__tests__/helpers/mock-istty.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const ORIGINAL_ISTTY = process.stdout.isTTY;
1+
const ORIGINAL_STDOUT_ISTTY = process.stdout.isTTY;
2+
const ORIGINAL_STDIN_ISTTY = process.stdin.isTTY;
23

34
/**
45
* Mock `process.stdout.isTTY`
@@ -9,14 +10,17 @@ export function useMockIsTTY() {
910
*/
1011
const setIsTTY = (isTTY: boolean) => {
1112
process.stdout.isTTY = isTTY;
13+
process.stdin.isTTY = isTTY;
1214
};
1315

1416
beforeEach(() => {
15-
process.stdout.isTTY = ORIGINAL_ISTTY;
17+
process.stdout.isTTY = ORIGINAL_STDOUT_ISTTY;
18+
process.stdin.isTTY = ORIGINAL_STDIN_ISTTY;
1619
});
1720

1821
afterEach(() => {
19-
process.stdout.isTTY = ORIGINAL_ISTTY;
22+
process.stdout.isTTY = ORIGINAL_STDOUT_ISTTY;
23+
process.stdin.isTTY = ORIGINAL_STDIN_ISTTY;
2024
});
2125

2226
return { setIsTTY };

packages/wrangler/src/__tests__/helpers/mock-oauth-flow.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fetchMock from "jest-fetch-mock";
22
import { Request } from "undici";
3+
import { getCloudflareApiBaseUrl } from "../../cfetch";
34
import openInBrowser from "../../open-in-browser";
45
import { mockHttpServer } from "./mock-http-server";
56

@@ -85,6 +86,28 @@ export const mockOAuthFlow = () => {
8586
return outcome;
8687
};
8788

89+
const mockGetMemberships = (args: {
90+
success: boolean;
91+
result: { id: string; account: { id: string; name: string } }[];
92+
}) => {
93+
const outcome = {
94+
actual: new Request("https://example.org"),
95+
expected: new Request(`${getCloudflareApiBaseUrl()}/memberships`, {
96+
method: "GET",
97+
}),
98+
};
99+
100+
fetchMock.mockIf(outcome.expected.url, async (req) => {
101+
outcome.actual = req;
102+
return {
103+
status: 200,
104+
body: JSON.stringify(args),
105+
};
106+
});
107+
108+
return outcome;
109+
};
110+
88111
const mockGrantAccessToken = ({
89112
respondWith,
90113
}: {
@@ -150,10 +173,11 @@ export const mockOAuthFlow = () => {
150173
};
151174

152175
return {
153-
mockOAuthServerCallback,
176+
mockGetMemberships,
177+
mockGrantAccessToken,
154178
mockGrantAuthorization,
179+
mockOAuthServerCallback,
155180
mockRevokeAuthorization,
156-
mockGrantAccessToken,
157181
mockExchangeRefreshTokenForAccessToken,
158182
};
159183
};

packages/wrangler/src/__tests__/publish.test.ts

+126
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
unsetMockFetchKVGetValues,
1212
} from "./helpers/mock-cfetch";
1313
import { mockConsoleMethods } from "./helpers/mock-console";
14+
import { useMockIsTTY } from "./helpers/mock-istty";
1415
import { mockKeyListRequest } from "./helpers/mock-kv";
1516
import { mockOAuthFlow } from "./helpers/mock-oauth-flow";
1617
import { runInTempDir } from "./helpers/run-in-tmp";
@@ -27,18 +28,21 @@ describe("publish", () => {
2728
mockAccountId();
2829
mockApiToken();
2930
runInTempDir({ homedir: "./home" });
31+
const { setIsTTY } = useMockIsTTY();
3032
const std = mockConsoleMethods();
3133
const {
3234
mockOAuthServerCallback,
3335
mockGrantAccessToken,
3436
mockGrantAuthorization,
37+
mockGetMemberships,
3538
} = mockOAuthFlow();
3639

3740
beforeEach(() => {
3841
// @ts-expect-error we're using a very simple setTimeout mock here
3942
jest.spyOn(global, "setTimeout").mockImplementation((fn, _period) => {
4043
setImmediate(fn);
4144
});
45+
setIsTTY(true);
4246
});
4347

4448
afterEach(() => {
@@ -56,6 +60,7 @@ describe("publish", () => {
5660
});
5761

5862
it("drops a user into the login flow if they're unauthenticated", async () => {
63+
// Should not throw missing Errors in TTY environment
5964
writeWranglerToml();
6065
writeWorkerSource();
6166
mockSubDomainRequest();
@@ -116,6 +121,127 @@ describe("publish", () => {
116121
`);
117122
expect(std.err).toMatchInlineSnapshot(`""`);
118123
});
124+
125+
describe("non-TTY", () => {
126+
const ENV_COPY = process.env;
127+
128+
afterEach(() => {
129+
process.env = ENV_COPY;
130+
});
131+
132+
it("should not throw an error in non-TTY if 'CLOUDFLARE_API_TOKEN' & 'account_id' are in scope", async () => {
133+
process.env = {
134+
CLOUDFLARE_API_TOKEN: "123456789",
135+
};
136+
setIsTTY(false);
137+
writeWranglerToml({
138+
account_id: "some-account-id",
139+
});
140+
writeWorkerSource();
141+
mockSubDomainRequest();
142+
mockUploadWorkerRequest();
143+
mockOAuthServerCallback();
144+
145+
await runWrangler("publish index.js");
146+
147+
expect(std.out).toMatchInlineSnapshot(`
148+
"Uploaded test-name (TIMINGS)
149+
Published test-name (TIMINGS)
150+
test-name.test-sub-domain.workers.dev"
151+
`);
152+
expect(std.err).toMatchInlineSnapshot(`""`);
153+
});
154+
155+
it("should not throw an error if 'CLOUDFLARE_ACCOUNT_ID' & 'CLOUDFLARE_API_TOKEN' are in scope", async () => {
156+
process.env = {
157+
CLOUDFLARE_API_TOKEN: "hunter2",
158+
CLOUDFLARE_ACCOUNT_ID: "some-account-id",
159+
};
160+
setIsTTY(false);
161+
writeWranglerToml();
162+
writeWorkerSource();
163+
mockSubDomainRequest();
164+
mockUploadWorkerRequest();
165+
mockOAuthServerCallback();
166+
mockGetMemberships({
167+
success: true,
168+
result: [],
169+
});
170+
171+
await runWrangler("publish index.js");
172+
173+
expect(std.out).toMatchInlineSnapshot(`
174+
"Uploaded test-name (TIMINGS)
175+
Published test-name (TIMINGS)
176+
test-name.test-sub-domain.workers.dev"
177+
`);
178+
expect(std.err).toMatchInlineSnapshot(`""`);
179+
});
180+
181+
it("should throw an error in non-TTY if 'account_id' & 'CLOUDFLARE_ACCOUNT_ID' is missing", async () => {
182+
setIsTTY(false);
183+
process.env = {
184+
CLOUDFLARE_API_TOKEN: "hunter2",
185+
CLOUDFLARE_ACCOUNT_ID: undefined,
186+
};
187+
writeWranglerToml({
188+
account_id: undefined,
189+
});
190+
writeWorkerSource();
191+
mockSubDomainRequest();
192+
mockUploadWorkerRequest();
193+
mockOAuthServerCallback();
194+
mockGetMemberships({
195+
success: true,
196+
result: [
197+
{ id: "IG-88", account: { id: "1701", name: "enterprise" } },
198+
{ id: "R2-D2", account: { id: "nx01", name: "enterprise-nx" } },
199+
],
200+
});
201+
202+
await expect(runWrangler("publish index.js")).rejects
203+
.toMatchInlineSnapshot(`
204+
[Error: More than one account available but unable to select one in non-interactive mode.
205+
Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.
206+
Available accounts are ("<name>" - "<id>"):
207+
"enterprise" - "1701")
208+
"enterprise-nx" - "nx01")]
209+
`);
210+
});
211+
212+
it("should throw error in non-TTY if 'CLOUDFLARE_API_TOKEN' is missing", async () => {
213+
setIsTTY(false);
214+
writeWranglerToml({
215+
account_id: undefined,
216+
});
217+
process.env = {
218+
CLOUDFLARE_API_TOKEN: undefined,
219+
CLOUDFLARE_ACCOUNT_ID: "badwolf",
220+
};
221+
writeWorkerSource();
222+
mockSubDomainRequest();
223+
mockUploadWorkerRequest();
224+
mockOAuthServerCallback();
225+
mockGetMemberships({
226+
success: true,
227+
result: [
228+
{ id: "IG-88", account: { id: "1701", name: "enterprise" } },
229+
{ id: "R2-D2", account: { id: "nx01", name: "enterprise-nx" } },
230+
],
231+
});
232+
233+
await expect(runWrangler("publish index.js")).rejects.toThrowError();
234+
235+
expect(std.err).toMatchInlineSnapshot(`
236+
"X [ERROR] Missing 'CLOUDFLARE_API_TOKEN' from non-TTY environment, please see docs for more info: TBD
237+
238+
239+
X [ERROR] Did not login, quitting...
240+
241+
"
242+
`);
243+
});
244+
});
119245
});
120246

121247
describe("environments", () => {

packages/wrangler/src/__tests__/secret.test.ts

+27-22
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import * as fs from "node:fs";
22
import * as TOML from "@iarna/toml";
3-
import fetchMock from "jest-fetch-mock";
43
import { mockAccountId, mockApiToken } from "./helpers/mock-account-id";
54
import { setMockResponse, unsetAllMocks } from "./helpers/mock-cfetch";
65
import { mockConsoleMethods } from "./helpers/mock-console";
76
import { mockConfirm, mockPrompt } from "./helpers/mock-dialogs";
7+
import { useMockIsTTY } from "./helpers/mock-istty";
8+
import { mockOAuthFlow } from "./helpers/mock-oauth-flow";
89
import { useMockStdin } from "./helpers/mock-stdin";
910
import { runInTempDir } from "./helpers/run-in-tmp";
1011
import { runWrangler } from "./helpers/run-wrangler";
1112

1213
describe("wrangler secret", () => {
1314
const std = mockConsoleMethods();
15+
const { mockGetMemberships } = mockOAuthFlow();
16+
const { setIsTTY } = useMockIsTTY();
1417
runInTempDir();
1518
mockAccountId();
1619
mockApiToken();
1720

1821
afterEach(() => {
1922
unsetAllMocks();
23+
setIsTTY(true);
2024
});
2125

2226
describe("put", () => {
@@ -169,6 +173,10 @@ describe("wrangler secret", () => {
169173
mockAccountId({ accountId: null });
170174

171175
it("should error if a user has no account", async () => {
176+
mockGetMemberships({
177+
success: false,
178+
result: [],
179+
});
172180
await expect(
173181
runWrangler("secret put the-key --name script-name")
174182
).rejects.toThrowErrorMatchingInlineSnapshot(
@@ -196,28 +204,25 @@ describe("wrangler secret", () => {
196204
});
197205

198206
it("should error if a user has multiple accounts, and has not specified an account in wrangler.toml", async () => {
199-
// This is a mock response for the request to the CF API memberships of the current user.
200-
fetchMock.doMockOnce(async () => {
201-
return {
202-
body: JSON.stringify({
203-
success: true,
204-
result: [
205-
{
206-
id: "1",
207-
account: { id: "account-id-1", name: "account-name-1" },
208-
},
209-
{
210-
id: "2",
211-
account: { id: "account-id-2", name: "account-name-2" },
212-
},
213-
{
214-
id: "3",
215-
account: { id: "account-id-3", name: "account-name-3" },
216-
},
217-
],
218-
}),
219-
};
207+
setIsTTY(false);
208+
mockGetMemberships({
209+
success: true,
210+
result: [
211+
{
212+
id: "1",
213+
account: { id: "account-id-1", name: "account-name-1" },
214+
},
215+
{
216+
id: "2",
217+
account: { id: "account-id-2", name: "account-name-2" },
218+
},
219+
{
220+
id: "3",
221+
account: { id: "account-id-3", name: "account-name-3" },
222+
},
223+
],
220224
});
225+
221226
await expect(runWrangler("secret put the-key --name script-name"))
222227
.rejects.toThrowErrorMatchingInlineSnapshot(`
223228
"More than one account available but unable to select one in non-interactive mode.

packages/wrangler/src/__tests__/sentry.test.ts

+8-12
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,27 @@ import * as Sentry from "@sentry/node";
77
import prompts from "prompts";
88

99
import { mockConsoleMethods } from "./helpers/mock-console";
10+
import { useMockIsTTY } from "./helpers/mock-istty";
1011
import { runInTempDir } from "./helpers/run-in-tmp";
1112
import { runWrangler } from "./helpers/run-wrangler";
1213
const { reportError } = jest.requireActual("../reporting");
1314

1415
describe("Error Reporting", () => {
16+
const { setIsTTY } = useMockIsTTY();
17+
1518
runInTempDir({ homedir: "./home" });
1619
mockConsoleMethods();
1720
const reportingTOMLPath = ".wrangler/config/reporting.toml";
1821

19-
const originalTTY = process.stdout.isTTY;
2022
beforeEach(() => {
2123
jest.mock("@sentry/node");
2224
jest.spyOn(Sentry, "captureException");
23-
process.stdout.isTTY = true;
25+
setIsTTY(true);
2426
});
2527

2628
afterEach(() => {
2729
jest.unmock("@sentry/node");
2830
jest.clearAllMocks();
29-
process.stdout.isTTY = originalTTY;
3031
});
3132

3233
it("should confirm user will allow error reporting usage", async () => {
@@ -127,20 +128,15 @@ describe("Error Reporting", () => {
127128
});
128129

129130
it("should not prompt in non-TTY environment", async () => {
130-
process.stdout.isTTY = false;
131-
131+
setIsTTY(false);
132132
await reportError(new Error("test error"), "testFalse");
133133

134-
const { error_tracking_opt, error_tracking_opt_date } = TOML.parse(
135-
await fsp.readFile(path.join(os.homedir(), reportingTOMLPath), "utf-8")
136-
);
137-
138-
expect(error_tracking_opt).toBe(false);
139-
expect(error_tracking_opt_date).toBeTruthy();
134+
expect(
135+
fs.existsSync(path.join(os.homedir(), reportingTOMLPath, "utf-8"))
136+
).toBe(false);
140137

141138
expect(Sentry.captureException).not.toHaveBeenCalledWith(
142139
new Error("test error")
143140
);
144-
process.stdout.isTTY = originalTTY;
145141
});
146142
});

0 commit comments

Comments
 (0)