-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Feat/appcheck debugtoken cli #10801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AustinBenoit
wants to merge
16
commits into
firebase:main
Choose a base branch
from
AustinBenoit:feat/appcheck-debugtoken-cli
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feat/appcheck debugtoken cli #10801
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
3478666
feat: Add App Check debug token management commands
andrewheard 82b99ec
feat: Implement firebase appcheck:debug <appId> to generate local .fi…
AustinBenoit e4b0c85
feat: align appcheck:debugtoken CLI commands with REST backend specif…
AustinBenoit af4791c
test: add command unit tests for appcheck:debugtoken CLI scenario suite
AustinBenoit 2923b69
fix: address code review comments for appcheck:debugtoken CLI and RES…
AustinBenoit 16f5b66
fix: resolve lint errors and formatting in appcheck commands and spec
AustinBenoit 32bf9cc
fix: stub requireAuth in spec and enable auth on App Check Client for…
AustinBenoit e703b3c
fix: bypass command pre-action hooks in spec for CI test runner
AustinBenoit 687fa9d
Fix nock matching for Node 24 by using RegExp
AustinBenoit 12e511a
Fix prettier formatting in tests
AustinBenoit c0f6c2c
fix(appcheck): stub client directly to bypass Node 24 nock fetch bug
AustinBenoit 759221c
feat(appcheck): refactor cli command namespace and args
AustinBenoit 12a1f85
feat(appcheck): require --force to overwrite existing token display n…
AustinBenoit c424e61
fix(lint): fix prettier formatting errors in appcheck-debugtokens-cre…
AustinBenoit 018b192
chore: remove old appcheck-debug files properly
AustinBenoit 06960f6
Refactor: extract App Check prompt utils and remove stale endpoints
AustinBenoit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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,56 @@ | ||
| import { appCheckOrigin } from "../api"; | ||
| import { Client } from "../apiv2"; | ||
| import { DebugToken, ListDebugTokensResponse } from "./types"; | ||
|
|
||
| const API_VERSION = "v1"; | ||
|
|
||
| export const client = new Client({ | ||
| urlPrefix: appCheckOrigin(), | ||
| auth: true, | ||
| apiVersion: API_VERSION, | ||
| }); | ||
|
|
||
| /** | ||
| * Creates a new DebugToken for the specified app. | ||
| */ | ||
| export async function createDebugToken( | ||
| projectNumber: string, | ||
| appId: string, | ||
| displayName: string, | ||
| token: string, | ||
| ): Promise<DebugToken> { | ||
| const parent = `projects/${projectNumber}/apps/${appId}`; | ||
| const res = await client.post<Partial<DebugToken>, DebugToken>(`${parent}/debugTokens`, { | ||
| displayName, | ||
| token, | ||
| }); | ||
| return res.body; | ||
| } | ||
|
|
||
| /** | ||
| * Lists all DebugTokens for the specified app. | ||
| */ | ||
| export async function listDebugTokens(projectNumber: string, appId: string): Promise<DebugToken[]> { | ||
| const parent = `projects/${projectNumber}/apps/${appId}`; | ||
| const debugTokens: DebugToken[] = []; | ||
| let pageToken = ""; | ||
| do { | ||
| const queryParams: Record<string, string> = {}; | ||
| if (pageToken) { | ||
| queryParams.pageToken = pageToken; | ||
| } | ||
| const res = await client.get<ListDebugTokensResponse>(`${parent}/debugTokens`, { queryParams }); | ||
| if (res.body?.debugTokens) { | ||
| debugTokens.push(...res.body.debugTokens); | ||
| } | ||
| pageToken = res.body?.nextPageToken || ""; | ||
| } while (pageToken); | ||
| return debugTokens; | ||
| } | ||
|
|
||
| /** | ||
| * Deletes the specified DebugToken. | ||
| */ | ||
| export async function deleteDebugToken(name: string): Promise<void> { | ||
| await client.delete<void>(name); | ||
| } |
This file contains hidden or 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,100 @@ | ||
| import { expect } from "chai"; | ||
| import * as sinon from "sinon"; | ||
|
|
||
| import { createDebugToken, listDebugTokens, deleteDebugToken, DebugToken, client } from "./index"; | ||
| import { FirebaseError } from "../error"; | ||
|
|
||
| describe("appcheck", () => { | ||
| const projectNumber = "123456789"; | ||
| const appId = "1:123456789:web:abc123def456"; | ||
| const parent = `projects/${projectNumber}/apps/${appId}`; | ||
| const debugTokenId = "debug-token-id-123"; | ||
| const debugTokenName = `${parent}/debugTokens/${debugTokenId}`; | ||
| const dummyDebugToken: DebugToken = { | ||
| name: debugTokenName, | ||
| displayName: "My Debug Token", | ||
| token: "00000000-0000-0000-0000-000000000000", | ||
| updateTime: "2023-01-01T00:00:00Z", | ||
| }; | ||
|
|
||
| let sandbox: sinon.SinonSandbox; | ||
|
|
||
| beforeEach(() => { | ||
| sandbox = sinon.createSandbox(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| sandbox.restore(); | ||
| }); | ||
|
|
||
| describe("createDebugToken", () => { | ||
| it("should resolve with created DebugToken on success", async () => { | ||
| const postStub = sandbox.stub(client, "post").resolves({ body: dummyDebugToken } as any); | ||
|
|
||
| const result = await createDebugToken( | ||
| projectNumber, | ||
| appId, | ||
| dummyDebugToken.displayName, | ||
| dummyDebugToken.token, | ||
| ); | ||
|
|
||
| expect(result).to.deep.equal(dummyDebugToken); | ||
| expect(postStub.calledOnce).to.be.true; | ||
| expect(postStub.firstCall.args[0]).to.match(/.*debugTokens.*/); | ||
| expect(postStub.firstCall.args[1]).to.deep.equal({ | ||
| displayName: dummyDebugToken.displayName, | ||
| token: dummyDebugToken.token, | ||
| }); | ||
| }); | ||
|
|
||
| it("should throw error on failure", async () => { | ||
| const postStub = sandbox.stub(client, "post").rejects(new FirebaseError("Invalid request")); | ||
|
|
||
| await expect( | ||
| createDebugToken(projectNumber, appId, dummyDebugToken.displayName, dummyDebugToken.token), | ||
| ).to.be.rejectedWith(FirebaseError, "Invalid request"); | ||
| expect(postStub.calledOnce).to.be.true; | ||
| }); | ||
| }); | ||
|
|
||
| describe("listDebugTokens", () => { | ||
| it("should resolve with list of DebugTokens on success", async () => { | ||
| const getStub = sandbox | ||
| .stub(client, "get") | ||
| .resolves({ body: { debugTokens: [dummyDebugToken] } } as any); | ||
|
|
||
| const result = await listDebugTokens(projectNumber, appId); | ||
| expect(result).to.deep.equal([dummyDebugToken]); | ||
| expect(getStub.calledOnce).to.be.true; | ||
| expect(getStub.firstCall.args[0]).to.match(/.*debugTokens.*/); | ||
| }); | ||
|
|
||
| it("should handle pagination", async () => { | ||
| const secondToken: DebugToken = { | ||
| ...dummyDebugToken, | ||
| name: `${parent}/debugTokens/token-2`, | ||
| }; | ||
| const getStub = sandbox.stub(client, "get"); | ||
| getStub | ||
| .onFirstCall() | ||
| .resolves({ body: { debugTokens: [dummyDebugToken], nextPageToken: "page-2" } } as any); | ||
| getStub.onSecondCall().resolves({ body: { debugTokens: [secondToken] } } as any); | ||
|
|
||
| const result = await listDebugTokens(projectNumber, appId); | ||
| expect(result).to.deep.equal([dummyDebugToken, secondToken]); | ||
| expect(getStub.calledTwice).to.be.true; | ||
| expect(getStub.secondCall.args[0]).to.match(/.*debugTokens.*/); | ||
| expect(getStub.secondCall.args[1]).to.deep.equal({ queryParams: { pageToken: "page-2" } }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("deleteDebugToken", () => { | ||
| it("should resolve on success", async () => { | ||
| const deleteStub = sandbox.stub(client, "delete").resolves({ body: {} } as any); | ||
|
|
||
| await expect(deleteDebugToken(debugTokenName)).to.be.eventually.fulfilled; | ||
| expect(deleteStub.calledOnce).to.be.true; | ||
| expect(deleteStub.firstCall.args[0]).to.match(/.*debugTokens.*/); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or 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,3 @@ | ||
| export * from "./types"; | ||
| export * from "./api"; | ||
| export * from "./prompts"; |
This file contains hidden or 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,52 @@ | ||
| import { AppPlatform, listFirebaseApps, selectAppInteractively } from "../management/apps"; | ||
| import { needProjectId } from "../projectUtils"; | ||
| import { detectApps } from "../appUtils"; | ||
| import { FirebaseError } from "../error"; | ||
| import { logger } from "../logger"; | ||
| import * as clc from "colorette"; | ||
| import { AppCheckDebugOptions } from "./types"; | ||
|
|
||
| /** | ||
| * Gets the appId from options or prompts the user to select an app if multiple exist. | ||
| * Uses needProjectId(options) to retrieve the active or specified project ID. | ||
| */ | ||
| export async function getOrPromptAppId( | ||
| options: AppCheckDebugOptions, | ||
| ): Promise<{ projectId: string; appId: string }> { | ||
| const projectId = needProjectId(options); | ||
|
|
||
| logger.info(`Active Project: ${clc.bold(projectId)}`); | ||
|
|
||
| if (options.app) { | ||
| return { projectId, appId: options.app }; | ||
| } | ||
|
|
||
| const projectDir = options.cwd || process.cwd(); | ||
| let apps = await listFirebaseApps(projectId, AppPlatform.ANY); | ||
| if (!apps.length) { | ||
| throw new FirebaseError(`There are no apps associated with project ${projectId}.`); | ||
| } | ||
|
|
||
| const localApps = await detectApps(projectDir); | ||
| const localAppIds = localApps.map((a) => a.appId).filter(Boolean) as string[]; | ||
| if (localAppIds.length > 0) { | ||
| const filteredApps = apps.filter((app) => localAppIds.includes(app.appId)); | ||
| if (filteredApps.length > 0) { | ||
| apps = filteredApps; | ||
| } | ||
| } | ||
|
|
||
| if (apps.length === 1) { | ||
| return { projectId, appId: apps[0].appId }; | ||
| } else if (options.nonInteractive) { | ||
| throw new FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`); | ||
| } | ||
|
|
||
| const selectedApp = await selectAppInteractively(apps, AppPlatform.ANY, { | ||
| message: "Select the app to register a debug token for:", | ||
| }); | ||
|
|
||
| return { projectId, appId: selectedApp.appId }; | ||
| } | ||
|
|
||
| export const getOrPromptProjectAndAppId = getOrPromptAppId; | ||
This file contains hidden or 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,18 @@ | ||
| import { Options } from "../options"; | ||
|
|
||
| export interface DebugToken { | ||
| name: string; | ||
| displayName: string; | ||
| token: string; | ||
| updateTime?: string; | ||
| } | ||
|
|
||
| export interface ListDebugTokensResponse { | ||
| debugTokens?: DebugToken[]; | ||
| nextPageToken?: string; | ||
| } | ||
|
|
||
| export interface AppCheckDebugOptions extends Options { | ||
| app?: string; | ||
| displayName?: string; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I'm revieiwng this, I'm realizing that there is duplicated logic for selecting an app from the list of exisitng apps in 3 places in the CLI already 🤦 That's an oversight on my part, but we really ought to centralize this . I'm gonna send you a PR shortly to do so - please review and we can merge that before this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#10844