UNPKG

convex

Version:

Client for the Convex Cloud

1,513 lines (1,334 loc) 58 kB
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { nodeFs } from "../bundler/fs.js"; import { env } from "./env.js"; import { deploy } from "./deploy.js"; import { dev } from "./dev.js"; import { deploymentFetch, bigBrainAPI, bigBrainAPIMaybeThrows, } from "./lib/utils/utils.js"; import { readGlobalConfig } from "./lib/utils/globalConfig.js"; import { deployToDeployment } from "./lib/deploy2.js"; import { runPush } from "./lib/components.js"; import { readProjectConfig, getAuthKitConfig } from "./lib/config.js"; import { gitBranchFromEnvironment } from "./lib/envvars.js"; import { devAgainstDeployment } from "./lib/dev.js"; import { handleLocalDeployment, loadLocalDeploymentCredentials, } from "./lib/localDeployment/localDeployment.js"; import { validateOrSelectTeam, validateOrSelectProject, } from "./lib/utils/utils.js"; import { ensureLoggedIn } from "./lib/login.js"; vi.mock("../bundler/fs.js", async (importOriginal) => { const actual = await importOriginal<typeof import("../bundler/fs.js")>(); return { ...actual, nodeFs: { ...actual.nodeFs, exists: vi.fn().mockImplementation(() => { throw new Error("nodeFs.exists should be mocked in test"); }), readUtf8File: vi.fn().mockImplementation(() => { throw new Error("nodeFs.readUtf8File should be mocked in test"); }), }, }; }); // Mock typedPlatformClient GET function — can be configured per test const mockPlatformGet = vi.fn(); vi.mock("./lib/utils/utils.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/utils/utils.js")>(); return { ...actual, deploymentFetch: vi.fn(), ensureHasConvexDependency: vi.fn(), bigBrainAPI: vi.fn(), bigBrainAPIMaybeThrows: vi.fn(), validateOrSelectTeam: vi.fn(), validateOrSelectProject: vi.fn(), typedPlatformClient: vi.fn(() => ({ GET: mockPlatformGet })), }; }); vi.mock("./lib/localDeployment/run.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/localDeployment/run.js")>(); return { ...actual, withRunningBackend: vi.fn( async ({ action }: { action: () => Promise<void> }) => { await action(); }, ), assertLocalBackendRunning: vi.fn(), }; }); vi.mock("./lib/utils/globalConfig.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/utils/globalConfig.js")>(); return { ...actual, readGlobalConfig: vi.fn().mockReturnValue(null), }; }); vi.mock("dotenv", async (importOriginal) => { const actual = await importOriginal<typeof import("dotenv")>(); return { ...actual, config: vi.fn(), }; }); vi.mock("@sentry/node", () => ({ captureException: vi.fn(), close: vi.fn(), })); // Deploy-specific mocks vi.mock("./lib/deploy2.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/deploy2.js")>(); return { ...actual, deployToDeployment: vi.fn(), }; }); vi.mock("./lib/components.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/components.js")>(); return { ...actual, runPush: vi.fn(), }; }); vi.mock("./lib/config.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/config.js")>(); return { ...actual, readProjectConfig: vi.fn().mockResolvedValue({ projectConfig: { functions: "convex" }, configPath: "convex.json", modules: [], }), getAuthKitConfig: vi.fn().mockResolvedValue(null), }; }); vi.mock("./lib/usage.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/usage.js")>(); return { ...actual, usageStateWarning: vi.fn(), }; }); vi.mock("./lib/updates.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/updates.js")>(); return { ...actual, checkVersion: vi.fn(), }; }); vi.mock("./lib/dev.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/dev.js")>(); return { ...actual, devAgainstDeployment: vi.fn() }; }); vi.mock("./lib/localDeployment/localDeployment.js", async (importOriginal) => { const actual = await importOriginal< typeof import("./lib/localDeployment/localDeployment.js") >(); return { ...actual, handleLocalDeployment: vi.fn(), loadLocalDeploymentCredentials: vi.fn(), }; }); vi.mock("./lib/login.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/login.js")>(); return { ...actual, ensureLoggedIn: vi.fn() }; }); vi.mock("./lib/aiFiles/index.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/aiFiles/index.js")>(); return { ...actual, maybeSetupAiFiles: vi.fn() }; }); vi.mock("./configure.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./configure.js")>(); return { ...actual, // Override to skip fetchDeploymentCanonicalSiteUrl and file-writing side // effects, while still exercising _deploymentCredentialsOrConfigure (which // routes through the BigBrain mocks, handleLocalDeployment, etc.). deploymentCredentialsOrConfigure: async ( ctx: any, deploymentSelection: any, chosenConfiguration: any, cmdOptions: any, ) => { const selected = await actual._deploymentCredentialsOrConfigure( ctx, deploymentSelection, chosenConfiguration, cmdOptions, ); return { url: selected.url, adminKey: selected.adminKey, deploymentFields: selected.deploymentFields !== null ? { ...selected.deploymentFields, siteUrl: null } : null, }; }, }; }); vi.mock("./lib/envvars.js", async (importOriginal) => { const actual = await importOriginal<typeof import("./lib/envvars.js")>(); return { ...actual, gitBranchFromEnvironment: vi.fn().mockReturnValue(null), isNonProdBuildEnvironment: vi.fn().mockReturnValue(false), }; }); /** * Routes mock Big Brain API calls by path. * Both `bigBrainAPI` and `bigBrainAPIMaybeThrows` delegate to this. */ function setupBigBrainRoutes(routes: Record<string, (data?: any) => any>) { const handler = (args: { path: string; data?: any }) => { // Match by exact path or by path prefix (for paths like `deployment/foo/team_and_project`) for (const [routePath, routeHandler] of Object.entries(routes)) { if (args.path === routePath || args.path.startsWith(routePath)) { return routeHandler(args.data); } } throw new Error(`Unmocked Big Brain route: ${args.path}`); }; vi.mocked(bigBrainAPI).mockImplementation(handler as any); vi.mocked(bigBrainAPIMaybeThrows).mockImplementation(handler as any); } describe("deployment selection flows", () => { let savedEnv: NodeJS.ProcessEnv; beforeEach(() => { savedEnv = { ...process.env }; process.env = {}; vi.resetAllMocks(); vi.mocked(readGlobalConfig).mockReturnValue(null); vi.mocked(nodeFs.exists).mockReturnValue(false); // Re-apply deploy-specific mocks after resetAllMocks vi.mocked(readProjectConfig).mockResolvedValue({ projectConfig: { functions: "convex" } as any, configPath: "convex.json", }); vi.mocked(getAuthKitConfig).mockResolvedValue(undefined); vi.mocked(gitBranchFromEnvironment).mockReturnValue(null); vi.mocked(devAgainstDeployment).mockResolvedValue(undefined); vi.mocked(handleLocalDeployment).mockResolvedValue({ deploymentName: "local-test", deploymentUrl: "http://127.0.0.1:3210", adminKey: "local|admin|key", onActivity: async () => {}, } as any); vi.mocked(loadLocalDeploymentCredentials).mockResolvedValue({ deploymentName: "local-test", deploymentUrl: "http://127.0.0.1:3210", adminKey: "local|admin|key", }); vi.mocked(validateOrSelectTeam).mockRejectedValue( new Error("validateOrSelectTeam should be mocked"), ); vi.mocked(validateOrSelectProject).mockRejectedValue( new Error("validateOrSelectProject should be mocked"), ); vi.mocked(ensureLoggedIn).mockResolvedValue(undefined); }); afterEach(() => { process.env = savedEnv; }); // Suppress process.exit and stderr beforeEach(() => { vi.spyOn(process, "exit").mockImplementation((() => { throw new Error("process.exit called"); }) as any); vi.spyOn(process.stderr, "write").mockImplementation(() => true); }); afterEach(() => { vi.restoreAllMocks(); }); describe("regular command (npx convex env)", () => { it("uses --url and --admin-key directly", async () => { const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( [ "set", "ABC", "DEF", "--url", "https://joyful-capybara-123.convex.cloud", "--admin-key", "my-admin-key", ], { from: "user" }, ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://joyful-capybara-123.convex.cloud", adminKey: "my-admin-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); // No Big Brain calls expect(bigBrainAPI).not.toHaveBeenCalled(); expect(bigBrainAPIMaybeThrows).not.toHaveBeenCalled(); }); it("resolves CONVEX_DEPLOY_KEY with deployment deploy key via Big Brain", async () => { process.env.CONVEX_DEPLOY_KEY = "prod:joyful-capybara-123|secretkey"; setupBigBrainRoutes({ "deployment/url_for_key": () => "https://joyful-capybara-123.eu-west-1.convex.cloud", "deployment/team_and_project_for_key": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF"], { from: "user" }); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://joyful-capybara-123.eu-west-1.convex.cloud", adminKey: "prod:joyful-capybara-123|secretkey", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/url_for_key" }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/team_and_project_for_key", }), ); }); it("resolves CONVEX_DEPLOY_KEY with project deploy key to dev deployment by default", async () => { process.env.CONVEX_DEPLOY_KEY = "project:identifier|secretkey"; setupBigBrainRoutes({ "deployment/provision_and_authorize": () => ({ adminKey: "dev-admin-key", url: "https://swift-squirrel-234.convex.cloud", deploymentName: "swift-squirrel-234", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF"], { from: "user" }); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://swift-squirrel-234.convex.cloud", adminKey: "dev-admin-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", }), ); }); it("resolves CONVEX_DEPLOY_KEY with project deploy key to prod deployment with --prod", async () => { process.env.CONVEX_DEPLOY_KEY = "project:identifier|secretkey"; setupBigBrainRoutes({ "deployment/provision_and_authorize": (_data: any) => ({ adminKey: "prod-admin-key", url: "https://graceful-puffin-456.convex.cloud", deploymentName: "graceful-puffin-456", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF", "--prod"], { from: "user" }); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://graceful-puffin-456.convex.cloud", adminKey: "prod-admin-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); // provision_and_authorize is called with prod deploymentType expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", data: expect.objectContaining({ deploymentType: "prod", }), }), ); }); it("uses CONVEX_SELF_HOSTED_URL and CONVEX_SELF_HOSTED_ADMIN_KEY directly", async () => { process.env.CONVEX_SELF_HOSTED_URL = "http://localhost:3210"; process.env.CONVEX_SELF_HOSTED_ADMIN_KEY = "self-hosted-key"; const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF"], { from: "user" }); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "http://localhost:3210", adminKey: "self-hosted-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); // No Big Brain calls expect(bigBrainAPI).not.toHaveBeenCalled(); expect(bigBrainAPIMaybeThrows).not.toHaveBeenCalled(); }); it("resolves CONVEX_DEPLOYMENT to dev deployment by default", async () => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/provision_and_authorize": () => ({ adminKey: "dev-key", url: "https://swift-squirrel-234.convex.cloud", deploymentName: "swift-squirrel-234", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF"], { from: "user" }); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://swift-squirrel-234.convex.cloud", adminKey: "dev-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); // checkAccessToSelectedProject calls getTeamAndProjectSlugForDeployment expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/joyful-capybara-123/team_and_project", method: "GET", }), ); }); it("resolves CONVEX_DEPLOYMENT with --prod to prod deployment", async () => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_prod": () => ({ adminKey: "prod-key", url: "https://graceful-puffin-456.convex.cloud", deploymentName: "graceful-puffin-456", deploymentType: "prod", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF", "--prod"], { from: "user" }); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://graceful-puffin-456.convex.cloud", adminKey: "prod-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_prod", }), ); }); it("resolves CONVEX_DEPLOYMENT with --preview-name to preview deployment", async () => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_preview": () => ({ adminKey: "preview-key", url: "https://nimble-penguin-234.convex.cloud", deploymentName: "nimble-penguin-234", deploymentType: "preview", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--preview-name", "my-preview"], { from: "user" }, ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://nimble-penguin-234.convex.cloud", adminKey: "preview-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_preview", }), ); }); it("resolves CONVEX_DEPLOYMENT with --deployment-name to named deployment", async () => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); setupBigBrainRoutes({ "deployment/limitless-wolf-571/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_within_current_project": () => ({ adminKey: "staging-key", url: "https://clever-otter-890.convex.cloud", deploymentName: "clever-otter-890", deploymentType: "dev", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment-name", "limitless-wolf-571"], { from: "user" }, ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://clever-otter-890.convex.cloud", adminKey: "staging-key", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", data: expect.objectContaining({ projectSelection: expect.objectContaining({ kind: "deploymentName", deploymentName: "limitless-wolf-571", deploymentType: null, }), }), }), ); }); it("resolves --deployment-name targeting a deployment in a different project from CONVEX_DEPLOYMENT", async () => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); setupBigBrainRoutes({ "deployment/cross-project-deploy/team_and_project": () => ({ team: "other-team", project: "other-project", teamId: 2, projectId: 2, }), "deployment/authorize_within_current_project": () => ({ adminKey: "cross-project-key", url: "https://cross-project-deploy.convex.cloud", deploymentName: "cross-project-deploy", deploymentType: "dev", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment-name", "cross-project-deploy"], { from: "user" }, ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://cross-project-deploy.convex.cloud", adminKey: "cross-project-key", }), ); // Verify authorize_within_current_project was called with the // --deployment-name deployment as the project selector, not CONVEX_DEPLOYMENT expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", data: expect.objectContaining({ projectSelection: expect.objectContaining({ kind: "deploymentName", deploymentName: "cross-project-deploy", deploymentType: null, }), selectedDeploymentName: "cross-project-deploy", }), }), ); }); it("resolves --deployment-name with cloud deployment name without CONVEX_DEPLOYMENT", async () => { delete process.env.CONVEX_DEPLOYMENT; setupBigBrainRoutes({ "deployment/clever-otter-890/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_within_current_project": () => ({ adminKey: "other-key", url: "https://clever-otter-890.convex.cloud", deploymentName: "clever-otter-890", deploymentType: "dev", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment-name", "clever-otter-890"], { from: "user" }, ); // The project was resolved using clever-otter-890 as the anchor, not // joyful-capybara-123 (which isn't set in this test). expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/clever-otter-890/team_and_project", }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", }), ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://clever-otter-890.convex.cloud", adminKey: "other-key", }), ); }); it("resolves deployment deploy key from --env-file", async () => { const fakeEnvFilePath = "/fake/convex-test.env"; vi.mocked(nodeFs.exists).mockReturnValue(true); vi.mocked(nodeFs.readUtf8File).mockReturnValue( "CONVEX_DEPLOY_KEY=prod:joyful-capybara-123|secretkey\n", ); setupBigBrainRoutes({ "deployment/url_for_key": () => "https://joyful-capybara-123.convex.cloud", "deployment/team_and_project_for_key": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--env-file", fakeEnvFilePath], { from: "user", }, ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://joyful-capybara-123.convex.cloud", adminKey: "prod:joyful-capybara-123|secretkey", }), ); expect(mockFetch).toHaveBeenCalledWith( "/api/update_environment_variables", expect.objectContaining({ method: "POST", body: JSON.stringify({ changes: [{ name: "ABC", value: "DEF" }] }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/url_for_key" }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/team_and_project_for_key", }), ); }); describe("--deployment flag", () => { beforeEach(() => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); }); it("resolves --deployment prod to production deployment", async () => { setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_prod": () => ({ adminKey: "prod-key", url: "https://graceful-puffin-456.convex.cloud", deploymentName: "graceful-puffin-456", deploymentType: "prod", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF", "--deployment", "prod"], { from: "user", }); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_prod" }), ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://graceful-puffin-456.convex.cloud", adminKey: "prod-key", }), ); }); it("resolves --deployment dev to dev deployment", async () => { setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/provision_and_authorize": () => ({ adminKey: "dev-key", url: "https://joyful-capybara-123.convex.cloud", deploymentName: "joyful-capybara-123", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF", "--deployment", "dev"], { from: "user", }); expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", data: expect.objectContaining({ deploymentType: "dev" }), }), ); }); it("resolves --deployment with cloud deployment name (abc-xyz-123)", async () => { setupBigBrainRoutes({ "deployment/clever-otter-890/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_within_current_project": () => ({ adminKey: "other-key", url: "https://clever-otter-890.convex.cloud", deploymentName: "clever-otter-890", deploymentType: "dev", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "clever-otter-890"], { from: "user" }, ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", data: expect.objectContaining({ selectedDeploymentName: "clever-otter-890", }), }), ); }); it("resolves --deployment with cloud deployment name without CONVEX_DEPLOYMENT", async () => { // No CONVEX_DEPLOYMENT set — the deployment name itself must be used as the // project anchor (team_and_project is looked up via clever-otter-890, not // via some pre-existing CONVEX_DEPLOYMENT). delete process.env.CONVEX_DEPLOYMENT; setupBigBrainRoutes({ "deployment/clever-otter-890/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_within_current_project": () => ({ adminKey: "other-key", url: "https://clever-otter-890.convex.cloud", deploymentName: "clever-otter-890", deploymentType: "dev", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "clever-otter-890"], { from: "user" }, ); // The project was resolved using clever-otter-890 as the anchor, not // joyful-capybara-123 (which isn't set in this test). expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/clever-otter-890/team_and_project", }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", data: expect.objectContaining({ selectedDeploymentName: "clever-otter-890", }), }), ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://clever-otter-890.convex.cloud", adminKey: "other-key", }), ); }); it("resolves --deployment with a reference", async () => { setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "teams/my-team/projects/my-project/deployments": () => true, "deployment/authorize_within_current_project": () => ({ adminKey: "staging-key", url: "https://clever-otter-890.convex.cloud", deploymentName: "clever-otter-890", deploymentType: "dev", }), }); mockPlatformGet.mockResolvedValue({ data: { name: "clever-otter-890" }, error: undefined, }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync(["set", "ABC", "DEF", "--deployment", "staging"], { from: "user", }); expect(mockPlatformGet).toHaveBeenCalledWith( "/teams/{team_id_or_slug}/projects/{project_slug}/deployment", expect.objectContaining({ params: expect.objectContaining({ path: { team_id_or_slug: "my-team", project_slug: "my-project" }, query: { reference: "staging" }, }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", data: expect.objectContaining({ selectedDeploymentName: "clever-otter-890", }), }), ); }); it("resolves --deployment with project:reference format", async () => { setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "teams/my-team/projects/other-project/deployments": () => true, "deployment/authorize_within_current_project": () => ({ adminKey: "other-proj-key", url: "https://other-deploy-123.convex.cloud", deploymentName: "other-deploy-123", deploymentType: "dev", }), }); mockPlatformGet.mockResolvedValue({ data: { name: "other-deploy-123" }, error: undefined, }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "other-project:staging"], { from: "user" }, ); expect(mockPlatformGet).toHaveBeenCalledWith( "/teams/{team_id_or_slug}/projects/{project_slug}/deployment", expect.objectContaining({ params: expect.objectContaining({ path: { team_id_or_slug: "my-team", project_slug: "other-project", }, query: { reference: "staging" }, }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", data: expect.objectContaining({ selectedDeploymentName: "other-deploy-123", }), }), ); }); it("resolves --deployment with team:project:reference format without CONVEX_DEPLOYMENT", async () => { // No CONVEX_DEPLOYMENT set delete process.env.CONVEX_DEPLOYMENT; setupBigBrainRoutes({ "deployment/authorize_within_current_project": () => ({ adminKey: "fq-key", url: "https://fully-qualified-123.convex.cloud", deploymentName: "fully-qualified-123", deploymentType: "dev", }), "teams/myteam/projects/myproj/deployments": () => true, }); mockPlatformGet.mockResolvedValue({ data: { name: "fully-qualified-123" }, error: undefined, }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "myteam:myproj:staging"], { from: "user" }, ); expect(mockPlatformGet).toHaveBeenCalledWith( "/teams/{team_id_or_slug}/projects/{project_slug}/deployment", expect.objectContaining({ params: expect.objectContaining({ path: { team_id_or_slug: "myteam", project_slug: "myproj" }, query: { reference: "staging" }, }), }), ); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_within_current_project", data: expect.objectContaining({ selectedDeploymentName: "fully-qualified-123", }), }), ); }); it("resolves --deployment project:dev to dev deployment in another project", async () => { setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/provision_and_authorize": () => ({ adminKey: "other-project-dev-key", url: "https://other-project-dev.convex.cloud", deploymentName: "other-project-dev", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "other-project:dev"], { from: "user" }, ); expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", data: expect.objectContaining({ teamSlug: "my-team", projectSlug: "other-project", deploymentType: "dev", }), }), ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://other-project-dev.convex.cloud", adminKey: "other-project-dev-key", }), ); }); it("resolves --deployment project:prod to prod deployment in another project", async () => { setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/provision_and_authorize": () => ({ adminKey: "other-project-prod-key", url: "https://other-project-prod.convex.cloud", deploymentName: "other-project-prod", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "other-project:prod"], { from: "user" }, ); expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", data: expect.objectContaining({ teamSlug: "my-team", projectSlug: "other-project", deploymentType: "prod", }), }), ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://other-project-prod.convex.cloud", adminKey: "other-project-prod-key", }), ); }); it("resolves --deployment team:project:prod to prod deployment in fully qualified team/project", async () => { setupBigBrainRoutes({ "teams/myteam/projects/myproject/deployments": () => true, "deployment/provision_and_authorize": () => ({ adminKey: "fq-prod-key", url: "https://fq-prod-deploy.convex.cloud", deploymentName: "fq-prod-deploy", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "myteam:myproject:prod"], { from: "user" }, ); expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", data: expect.objectContaining({ teamSlug: "myteam", projectSlug: "myproject", deploymentType: "prod", }), }), ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://fq-prod-deploy.convex.cloud", adminKey: "fq-prod-key", }), ); }); it("resolves --deployment team:project:dev to dev deployment in fully qualified team/project", async () => { setupBigBrainRoutes({ "teams/myteam/projects/myproject/deployments": () => true, "deployment/provision_and_authorize": () => ({ adminKey: "fq-dev-key", url: "https://fq-dev-deploy.convex.cloud", deploymentName: "fq-dev-deploy", }), }); const mockFetch = vi.fn().mockResolvedValue({ ok: true }); vi.mocked(deploymentFetch).mockReturnValue(mockFetch as any); await env.parseAsync( ["set", "ABC", "DEF", "--deployment", "myteam:myproject:dev"], { from: "user" }, ); expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", data: expect.objectContaining({ teamSlug: "myteam", projectSlug: "myproject", deploymentType: "dev", }), }), ); expect(deploymentFetch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ deploymentUrl: "https://fq-dev-deploy.convex.cloud", adminKey: "fq-dev-key", }), ); }); it("errors when --deployment used with self-hosted deployment", async () => { delete process.env.CONVEX_DEPLOYMENT; process.env.CONVEX_SELF_HOSTED_URL = "http://localhost:3210"; process.env.CONVEX_SELF_HOSTED_ADMIN_KEY = "self-hosted-key"; await expect( env.parseAsync(["set", "ABC", "DEF", "--deployment", "prod"], { from: "user", }), ).rejects.toThrow(); expect(deploymentFetch).not.toHaveBeenCalled(); }); it("errors when --deployment used with --url and --admin-key", async () => { await expect( env.parseAsync( [ "set", "ABC", "DEF", "--deployment", "prod", "--url", "https://example.convex.cloud", "--admin-key", "mykey", ], { from: "user" }, ), ).rejects.toThrow(); }); }); }); describe("deploy command (npx convex deploy)", () => { it("defaults to prod with CONVEX_DEPLOYMENT (implicitProd)", async () => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_prod": () => ({ adminKey: "prod-key", url: "https://graceful-puffin-456.convex.cloud", deploymentName: "graceful-puffin-456", deploymentType: "prod", }), }); await deploy.parseAsync(["--yes"], { from: "user" }); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_prod", }), ); expect(deployToDeployment).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ url: "https://graceful-puffin-456.convex.cloud", adminKey: "prod-key", }), expect.anything(), ); }); it("defaults to prod with project deploy key", async () => { process.env.CONVEX_DEPLOY_KEY = "project:identifier|secretkey"; setupBigBrainRoutes({ "deployment/provision_and_authorize": () => ({ adminKey: "prod-admin-key", url: "https://graceful-puffin-456.convex.cloud", deploymentName: "graceful-puffin-456", }), }); await deploy.parseAsync([], { from: "user" }); expect(bigBrainAPIMaybeThrows).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/provision_and_authorize", data: expect.objectContaining({ deploymentType: "prod", }), }), ); expect(deployToDeployment).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ url: "https://graceful-puffin-456.convex.cloud", adminKey: "prod-admin-key", }), expect.anything(), ); }); it("deploys to preview with preview deploy key and --preview-create", async () => { process.env.CONVEX_DEPLOY_KEY = "preview:my-team:my-project|secretkey"; setupBigBrainRoutes({ claim_preview_deployment: () => ({ adminKey: "preview-admin-key", instanceUrl: "https://nimble-penguin-234.convex.cloud", }), }); await deploy.parseAsync(["--preview-create", "my-preview"], { from: "user", }); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "claim_preview_deployment", data: expect.objectContaining({ identifier: "my-preview", projectSelection: { kind: "teamAndProjectSlugs", teamSlug: "my-team", projectSlug: "my-project", }, }), }), ); expect(runPush).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ url: "https://nimble-penguin-234.convex.cloud", adminKey: "preview-admin-key", }), ); }); it("deploys to preview with preview deploy key using git branch fallback", async () => { process.env.CONVEX_DEPLOY_KEY = "preview:my-team:my-project|secretkey"; vi.mocked(gitBranchFromEnvironment).mockReturnValue("feature/my-branch"); setupBigBrainRoutes({ claim_preview_deployment: () => ({ adminKey: "preview-admin-key", instanceUrl: "https://nimble-penguin-234.convex.cloud", }), }); await deploy.parseAsync([], { from: "user" }); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "claim_preview_deployment", data: expect.objectContaining({ identifier: "feature/my-branch", }), }), ); expect(runPush).toHaveBeenCalled(); }); it("deploys to existing preview with CONVEX_DEPLOYMENT and --preview-name", async () => { process.env.CONVEX_DEPLOYMENT = "dev:joyful-capybara-123"; vi.mocked(readGlobalConfig).mockReturnValue({ accessToken: "test-token", }); setupBigBrainRoutes({ "deployment/joyful-capybara-123/team_and_project": () => ({ team: "my-team", project: "my-project", teamId: 1, projectId: 1, }), "deployment/authorize_preview": () => ({ adminKey: "preview-key", url: "https://nimble-penguin-234.convex.cloud", deploymentName: "nimble-penguin-234", deploymentType: "preview", }), }); await deploy.parseAsync(["--preview-name", "my-preview", "--yes"], { from: "user", }); expect(bigBrainAPI).toHaveBeenCalledWith( expect.objectContaining({ path: "deployment/authorize_preview", }), ); expect(deployToDeployment).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ url: "https://nimble-penguin-234.convex.cloud", adminKey: "preview-key", }), expect.anything(), ); }); it("crashes w