Skip to content

Commit

Permalink
feat: add whoami command
Browse files Browse the repository at this point in the history
This commit adds a new `whoami` command to the CLI, which aligns with Wrangler 1.

Resolves cloudflare#274
  • Loading branch information
petebacondarwin committed Jan 24, 2022
1 parent 007dc36 commit f120af7
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-comics-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

Add whoami command
2 changes: 2 additions & 0 deletions packages/wrangler/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe("wrangler", () => {
Commands:
wrangler init [name] 📥 Create a wrangler.toml configuration file
wrangler whoami 🕵️ Retrieve your user info and test your auth config
wrangler dev <filename> 👂 Start a local server for developing your worker
wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
Expand Down Expand Up @@ -49,6 +50,7 @@ describe("wrangler", () => {
Commands:
wrangler init [name] 📥 Create a wrangler.toml configuration file
wrangler whoami 🕵️ Retrieve your user info and test your auth config
wrangler dev <filename> 👂 Start a local server for developing your worker
wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
Expand Down
138 changes: 138 additions & 0 deletions packages/wrangler/src/__tests__/whoami.test.tsx
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,
"?"
);
}
8 changes: 4 additions & 4 deletions packages/wrangler/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import onExit from "signal-exit";
import { setTimeout } from "node:timers/promises";
import * as fs from "node:fs";
import { execa } from "execa";
import { whoami } from "./whoami";

const resetColor = "\x1b[0m";
const fgGreenColor = "\x1b[32m";
Expand Down Expand Up @@ -364,11 +365,10 @@ export async function main(argv: string[]): Promise<void> {
// whoami
wrangler.command(
"whoami",
false, // we don't need to show this the menu
// "🕵️ Retrieve your user info and test your auth config",
"🕵️ Retrieve your user info and test your auth config",
() => {},
(args) => {
console.log(":whoami", args);
async () => {
await whoami();
}
);

Expand Down
69 changes: 69 additions & 0 deletions packages/wrangler/src/whoami.tsx
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 &apos;{props.email}&apos;!
</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");
}

0 comments on commit f120af7

Please sign in to comment.