Skip to content

Commit 6283ad5

Browse files
authored
non-TTY required variables checks (#852)
* 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 * 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 d0801b7 commit 6283ad5

File tree

12 files changed

+271
-58
lines changed

12 files changed

+271
-58
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

+148
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
unsetMockFetchKVGetValues,
1212
} from "./helpers/mock-cfetch";
1313
import { mockConsoleMethods, normalizeSlashes } 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(() => {
@@ -116,6 +120,150 @@ describe("publish", () => {
116120
`);
117121
expect(std.err).toMatchInlineSnapshot(`""`);
118122
});
123+
124+
describe("non-TTY", () => {
125+
const ENV_COPY = process.env;
126+
127+
afterEach(() => {
128+
process.env = ENV_COPY;
129+
});
130+
131+
it("should not throw an error in non-TTY if 'CLOUDFLARE_API_TOKEN' & 'account_id' are in scope", async () => {
132+
process.env = {
133+
CLOUDFLARE_API_TOKEN: "123456789",
134+
};
135+
setIsTTY(false);
136+
writeWranglerToml({
137+
account_id: "some-account-id",
138+
});
139+
writeWorkerSource();
140+
mockSubDomainRequest();
141+
mockUploadWorkerRequest();
142+
mockOAuthServerCallback();
143+
144+
await runWrangler("publish index.js");
145+
146+
expect(std.out).toMatchInlineSnapshot(`
147+
"Uploaded test-name (TIMINGS)
148+
Published test-name (TIMINGS)
149+
test-name.test-sub-domain.workers.dev"
150+
`);
151+
expect(std.err).toMatchInlineSnapshot(`""`);
152+
});
153+
154+
it("should not throw an error if 'CLOUDFLARE_ACCOUNT_ID' & 'CLOUDFLARE_API_TOKEN' are in scope", async () => {
155+
process.env = {
156+
CLOUDFLARE_API_TOKEN: "hunter2",
157+
CLOUDFLARE_ACCOUNT_ID: "some-account-id",
158+
};
159+
setIsTTY(false);
160+
writeWranglerToml();
161+
writeWorkerSource();
162+
mockSubDomainRequest();
163+
mockUploadWorkerRequest();
164+
mockOAuthServerCallback();
165+
mockGetMemberships({
166+
success: true,
167+
result: [],
168+
});
169+
170+
await runWrangler("publish index.js");
171+
172+
expect(std.out).toMatchInlineSnapshot(`
173+
"Uploaded test-name (TIMINGS)
174+
Published test-name (TIMINGS)
175+
test-name.test-sub-domain.workers.dev"
176+
`);
177+
expect(std.err).toMatchInlineSnapshot(`""`);
178+
});
179+
180+
it("should throw an error in non-TTY & there is more than one account associated with API token", async () => {
181+
setIsTTY(false);
182+
process.env = {
183+
CLOUDFLARE_API_TOKEN: "hunter2",
184+
CLOUDFLARE_ACCOUNT_ID: undefined,
185+
};
186+
writeWranglerToml({
187+
account_id: undefined,
188+
});
189+
writeWorkerSource();
190+
mockSubDomainRequest();
191+
mockUploadWorkerRequest();
192+
mockOAuthServerCallback();
193+
mockGetMemberships({
194+
success: true,
195+
result: [
196+
{ id: "IG-88", account: { id: "1701", name: "enterprise" } },
197+
{ id: "R2-D2", account: { id: "nx01", name: "enterprise-nx" } },
198+
],
199+
});
200+
201+
await expect(runWrangler("publish index.js")).rejects
202+
.toMatchInlineSnapshot(`
203+
[Error: More than one account available but unable to select one in non-interactive mode.
204+
Please set the appropriate \`account_id\` in your \`wrangler.toml\` file.
205+
Available accounts are ("<name>" - "<id>"):
206+
"enterprise" - "1701")
207+
"enterprise-nx" - "nx01")]
208+
`);
209+
});
210+
211+
it("should throw error in non-TTY if 'CLOUDFLARE_API_TOKEN' is missing", async () => {
212+
setIsTTY(false);
213+
writeWranglerToml({
214+
account_id: undefined,
215+
});
216+
process.env = {
217+
CLOUDFLARE_API_TOKEN: undefined,
218+
CLOUDFLARE_ACCOUNT_ID: "badwolf",
219+
};
220+
writeWorkerSource();
221+
mockSubDomainRequest();
222+
mockUploadWorkerRequest();
223+
mockOAuthServerCallback();
224+
mockGetMemberships({
225+
success: true,
226+
result: [
227+
{ id: "IG-88", account: { id: "1701", name: "enterprise" } },
228+
{ id: "R2-D2", account: { id: "nx01", name: "enterprise-nx" } },
229+
],
230+
});
231+
232+
await expect(runWrangler("publish index.js")).rejects.toThrowError();
233+
234+
expect(std.err).toMatchInlineSnapshot(`
235+
"X [ERROR] In a non-interactive environment, it's necessary to set a CLOUDFLARE_API_TOKEN environment variable for wrangler to work. Please go to https://developers.cloudflare.com/api/tokens/create/ for instructions on how to create an api token, and assign its value to CLOUDFLARE_API_TOKEN.
236+
237+
"
238+
`);
239+
});
240+
it("should throw error with no account ID provided and no members retrieved", async () => {
241+
setIsTTY(false);
242+
writeWranglerToml({
243+
account_id: undefined,
244+
});
245+
process.env = {
246+
CLOUDFLARE_API_TOKEN: "picard",
247+
CLOUDFLARE_ACCOUNT_ID: undefined,
248+
};
249+
writeWorkerSource();
250+
mockSubDomainRequest();
251+
mockUploadWorkerRequest();
252+
mockOAuthServerCallback();
253+
mockGetMemberships({
254+
success: false,
255+
result: [],
256+
});
257+
258+
await expect(runWrangler("publish index.js")).rejects.toThrowError();
259+
260+
expect(std.err).toMatchInlineSnapshot(`
261+
"X [ERROR] Failed to automatically retrieve account IDs for the logged in user. In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your \`wrangler.toml\` file.
262+
263+
"
264+
`);
265+
});
266+
});
119267
});
120268

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

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

+25-23
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
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 { mockOAuthFlow } from "./helpers/mock-oauth-flow";
88
import { useMockStdin } from "./helpers/mock-stdin";
99
import { runInTempDir } from "./helpers/run-in-tmp";
1010
import { runWrangler } from "./helpers/run-wrangler";
1111

1212
describe("wrangler secret", () => {
1313
const std = mockConsoleMethods();
14+
const { mockGetMemberships } = mockOAuthFlow();
15+
1416
runInTempDir();
1517
mockAccountId();
1618
mockApiToken();
@@ -169,10 +171,14 @@ describe("wrangler secret", () => {
169171
mockAccountId({ accountId: null });
170172

171173
it("should error if a user has no account", async () => {
174+
mockGetMemberships({
175+
success: false,
176+
result: [],
177+
});
172178
await expect(
173179
runWrangler("secret put the-key --name script-name")
174180
).rejects.toThrowErrorMatchingInlineSnapshot(
175-
`"No account id found, quitting..."`
181+
`"Failed to automatically retrieve account IDs for the logged in user. In a non-interactive environment, it is mandatory to specify an account ID, either by assigning its value to CLOUDFLARE_ACCOUNT_ID, or as \`account_id\` in your \`wrangler.toml\` file."`
176182
);
177183
});
178184

@@ -196,28 +202,24 @@ describe("wrangler secret", () => {
196202
});
197203

198204
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-
};
205+
mockGetMemberships({
206+
success: true,
207+
result: [
208+
{
209+
id: "1",
210+
account: { id: "account-id-1", name: "account-name-1" },
211+
},
212+
{
213+
id: "2",
214+
account: { id: "account-id-2", name: "account-name-2" },
215+
},
216+
{
217+
id: "3",
218+
account: { id: "account-id-3", name: "account-name-3" },
219+
},
220+
],
220221
});
222+
221223
await expect(runWrangler("secret put the-key --name script-name"))
222224
.rejects.toThrowErrorMatchingInlineSnapshot(`
223225
"More than one account available but unable to select one in non-interactive mode.

0 commit comments

Comments
 (0)