forked from cloudflare/workers-sdk
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit adds a new `whoami` command to the CLI, which aligns with Wrangler 1. Resolves cloudflare#274
- Loading branch information
1 parent
007dc36
commit f120af7
Showing
5 changed files
with
218 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"wrangler": patch | ||
--- | ||
|
||
Add whoami command |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import React from "react"; | ||
import os from "node:os"; | ||
import path from "node:path"; | ||
import { render } from "ink-testing-library"; | ||
import type { UserInfo } from "../whoami"; | ||
import { getUserInfo, WhoAmI } from "../whoami"; | ||
import { runInTempDir } from "./run-in-tmp"; | ||
import { mkdirSync, writeFileSync } from "node:fs"; | ||
import { setMockResponse } from "./mock-cfetch"; | ||
import { initialise } from "../user"; | ||
|
||
const ORIGINAL_CF_API_TOKEN = process.env.CF_API_TOKEN; | ||
const ORIGINAL_CF_ACCOUNT_ID = process.env.CF_ACCOUNT_ID; | ||
|
||
describe("getUserInfo()", () => { | ||
runInTempDir(); | ||
|
||
beforeEach(() => { | ||
// Clear the environment variables, so we can control them in the tests | ||
delete process.env.CF_API_TOKEN; | ||
delete process.env.CF_ACCOUNT_ID; | ||
// Override where the home directory is so that we can specify a user config | ||
mkdirSync("./home"); | ||
jest.spyOn(os, "homedir").mockReturnValue("./home"); | ||
}); | ||
|
||
afterEach(() => { | ||
// Reset any changes to the environment variables | ||
process.env.CF_API_TOKEN = ORIGINAL_CF_API_TOKEN; | ||
process.env.CF_ACCOUNT_ID = ORIGINAL_CF_ACCOUNT_ID; | ||
}); | ||
|
||
it("should return undefined if there is no config file", async () => { | ||
await initialise(); | ||
const userInfo = await getUserInfo(); | ||
expect(userInfo).toBeUndefined(); | ||
}); | ||
|
||
it("should return undefined if there is an empty config file", async () => { | ||
writeUserConfig(); | ||
await initialise(); | ||
const userInfo = await getUserInfo(); | ||
expect(userInfo).toBeUndefined(); | ||
}); | ||
|
||
it("should return the user's email and accounts if authenticated via config token", async () => { | ||
writeUserConfig("some-oauth-token"); | ||
setMockResponse("/user", () => { | ||
return { email: "user@example.com" }; | ||
}); | ||
setMockResponse("/accounts", () => { | ||
return [ | ||
{ name: "Account One", id: "account-1" }, | ||
{ name: "Account Two", id: "account-2" }, | ||
{ name: "Account Three", id: "account-3" }, | ||
]; | ||
}); | ||
|
||
await initialise(); | ||
const userInfo = await getUserInfo(); | ||
|
||
expect(userInfo).toEqual({ | ||
authType: "OAuth", | ||
apiToken: "some-oauth-token", | ||
email: "user@example.com", | ||
accounts: [ | ||
{ name: "Account One", id: "account-1" }, | ||
{ name: "Account Two", id: "account-2" }, | ||
{ name: "Account Three", id: "account-3" }, | ||
], | ||
}); | ||
}); | ||
}); | ||
|
||
describe("WhoAmI component", () => { | ||
it("should return undefined if there is no user", async () => { | ||
const { lastFrame } = render(<WhoAmI user={undefined}></WhoAmI>); | ||
|
||
expect(lastFrame()).toMatchInlineSnapshot( | ||
`"You are not authenticated. Please run \`wrangler login\`."` | ||
); | ||
}); | ||
|
||
it("should display the user's email and accounts", async () => { | ||
const user: UserInfo = { | ||
authType: "OAuth", | ||
apiToken: "some-oauth-token", | ||
email: "user@example.com", | ||
accounts: [ | ||
{ name: "Account One", id: "account-1" }, | ||
{ name: "Account Two", id: "account-2" }, | ||
{ name: "Account Three", id: "account-3" }, | ||
], | ||
}; | ||
|
||
const { lastFrame } = render(<WhoAmI user={user}></WhoAmI>); | ||
|
||
expect(lastFrame()).toContain( | ||
"You are logged in with an OAuth Token, associated with the email 'user@example.com'!" | ||
); | ||
expect(lastFrame()).toMatch(/Account Name .+ Account ID/); | ||
expect(lastFrame()).toMatch(/Account One .+ account-1/); | ||
expect(lastFrame()).toMatch(/Account Two .+ account-2/); | ||
expect(lastFrame()).toMatch(/Account Three .+ account-3/); | ||
}); | ||
}); | ||
|
||
function writeUserConfig( | ||
oauth_token?: string, | ||
refresh_token?: string, | ||
expiration_time?: string | ||
) { | ||
const lines: string[] = []; | ||
if (oauth_token) { | ||
lines.push(`oauth_token = "${oauth_token}"`); | ||
} | ||
if (refresh_token) { | ||
lines.push(`refresh_token = "${refresh_token}"`); | ||
} | ||
if (expiration_time) { | ||
lines.push(`expiration_time = "${expiration_time}"`); | ||
} | ||
const configPath = path.join(os.homedir(), ".wrangler/config"); | ||
mkdirSync(configPath, { recursive: true }); | ||
writeFileSync( | ||
path.join(configPath, "default.toml"), | ||
lines.join("\n"), | ||
"utf-8" | ||
); | ||
} | ||
|
||
function stripControlCharacters(str = ""): string { | ||
return str.replace( | ||
// eslint-disable-next-line no-control-regex | ||
/[^\n\s!-~]|(\[\d+m)/g, | ||
"?" | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Text, render } from "ink"; | ||
import Table from "ink-table"; | ||
import React from "react"; | ||
import { fetchListResult, fetchResult } from "./cfetch"; | ||
import { getAPIToken } from "./user"; | ||
|
||
export async function whoami() { | ||
console.log("Getting User settings..."); | ||
const user = await getUserInfo(); | ||
render(<WhoAmI user={user}></WhoAmI>); | ||
} | ||
|
||
export function WhoAmI({ user }: { user: UserInfo | undefined }) { | ||
return user ? ( | ||
<> | ||
<Email tokenType={user.authType} email={user.email}></Email> | ||
<Accounts accounts={user.accounts}></Accounts> | ||
</> | ||
) : ( | ||
<Text>You are not authenticated. Please run `wrangler login`.</Text> | ||
); | ||
} | ||
|
||
function Email(props: { tokenType: string; email: string }) { | ||
return ( | ||
<Text> | ||
👋 You are logged in with an {props.tokenType} Token, associated with the | ||
email '{props.email}'! | ||
</Text> | ||
); | ||
} | ||
|
||
function Accounts(props: { accounts: AccountInfo[] }) { | ||
const accounts = props.accounts.map((account) => ({ | ||
"Account Name": account.name, | ||
"Account ID": account.id, | ||
})); | ||
return <Table data={accounts}></Table>; | ||
} | ||
|
||
export interface UserInfo { | ||
apiToken: string; | ||
authType: string; | ||
email: string; | ||
accounts: AccountInfo[]; | ||
} | ||
|
||
export async function getUserInfo(): Promise<UserInfo | undefined> { | ||
const apiToken = getAPIToken(); | ||
return apiToken | ||
? { | ||
apiToken, | ||
authType: "OAuth", | ||
email: await getEmail(), | ||
accounts: await getAccounts(), | ||
} | ||
: undefined; | ||
} | ||
|
||
async function getEmail(): Promise<string> { | ||
const { email } = await fetchResult<{ email: string }>("/user"); | ||
return email; | ||
} | ||
|
||
type AccountInfo = { name: string; id: string }; | ||
|
||
async function getAccounts(): Promise<AccountInfo[]> { | ||
return await fetchListResult<AccountInfo>("/accounts"); | ||
} |