diff --git a/src/cli/commands/connectors/index.ts b/src/cli/commands/connectors/index.ts new file mode 100644 index 00000000..191915b9 --- /dev/null +++ b/src/cli/commands/connectors/index.ts @@ -0,0 +1,9 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { getConnectorsPushCommand } from "./push.js"; + +export function getConnectorsCommand(context: CLIContext): Command { + return new Command("connectors") + .description("Manage project connectors (OAuth integrations)") + .addCommand(getConnectorsPushCommand(context)); +} diff --git a/src/cli/commands/connectors/push.ts b/src/cli/commands/connectors/push.ts new file mode 100644 index 00000000..55f04e61 --- /dev/null +++ b/src/cli/commands/connectors/push.ts @@ -0,0 +1,161 @@ +import { confirm, isCancel, log } from "@clack/prompts"; +import chalk from "chalk"; +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { runCommand, runTask } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { readProjectConfig } from "@/core/index.js"; +import { + type ConnectorOAuthStatus, + type ConnectorSyncResult, + type IntegrationType, + pushConnectors, + runOAuthFlow, +} from "@/core/resources/connector/index.js"; + +type PendingOAuthResult = ConnectorSyncResult & { + redirectUrl: string; + connectionId: string; +}; + +function isPendingOAuth(r: ConnectorSyncResult): r is PendingOAuthResult { + return r.action === "needs_oauth" && !!r.redirectUrl && !!r.connectionId; +} + +function printSummary( + results: ConnectorSyncResult[], + oauthOutcomes: Map +): void { + const synced: IntegrationType[] = []; + const added: IntegrationType[] = []; + const removed: IntegrationType[] = []; + const failed: { type: IntegrationType; error?: string }[] = []; + + for (const r of results) { + const oauthStatus = oauthOutcomes.get(r.type); + + if (r.action === "synced") { + synced.push(r.type); + } else if (r.action === "removed") { + removed.push(r.type); + } else if (r.action === "error") { + failed.push({ type: r.type, error: r.error }); + } else if (r.action === "needs_oauth") { + if (oauthStatus === "ACTIVE") { + added.push(r.type); + } else if (oauthStatus === "PENDING") { + failed.push({ type: r.type, error: "authorization timed out" }); + } else if (oauthStatus === "FAILED") { + failed.push({ type: r.type, error: "authorization failed" }); + } else { + failed.push({ type: r.type, error: "needs authorization" }); + } + } + } + + log.info(""); + log.info(chalk.bold("Summary:")); + + if (synced.length > 0) { + log.info(chalk.green(` Synced: ${synced.join(", ")}`)); + } + if (added.length > 0) { + log.info(chalk.green(` Added: ${added.join(", ")}`)); + } + if (removed.length > 0) { + log.info(chalk.dim(` Removed: ${removed.join(", ")}`)); + } + for (const r of failed) { + log.info(chalk.red(` Failed: ${r.type}${r.error ? ` - ${r.error}` : ""}`)); + } +} + +async function pushConnectorsAction(): Promise { + const { connectors } = await readProjectConfig(); + + if (connectors.length === 0) { + log.info( + "No local connectors found - checking for remote connectors to remove" + ); + } else { + const connectorNames = connectors.map((c) => c.type).join(", "); + log.info( + `Found ${connectors.length} connectors to push: ${connectorNames}` + ); + } + + const { results } = await runTask( + "Pushing connectors to Base44", + async () => { + return await pushConnectors(connectors); + }, + { + successMessage: "Connectors pushed", + errorMessage: "Failed to push connectors", + } + ); + + const oauthOutcomes = new Map(); + const needsOAuth = results.filter(isPendingOAuth); + let outroMessage = "Connectors pushed to Base44"; + + if (needsOAuth.length > 0) { + log.info(""); + log.info( + chalk.yellow( + `${needsOAuth.length} connector(s) require authorization in your browser:` + ) + ); + for (const connector of needsOAuth) { + log.info(` ${connector.type}: ${chalk.dim(connector.redirectUrl)}`); + } + + const pending = needsOAuth.map((c) => c.type).join(", "); + + if (process.env.CI) { + outroMessage = `Skipped OAuth in CI. Pending: ${pending}. Run 'base44 connectors push' locally to authorize.`; + } else { + const shouldAuth = await confirm({ + message: "Open browser to authorize now?", + }); + + if (isCancel(shouldAuth) || !shouldAuth) { + outroMessage = `Authorization skipped. Pending: ${pending}. Run 'base44 connectors push' again to complete.`; + } else { + for (const connector of needsOAuth) { + log.info(`\nOpening browser for ${connector.type}...`); + + const oauthResult = await runTask( + `Waiting for ${connector.type} authorization...`, + async () => { + return await runOAuthFlow({ + type: connector.type, + redirectUrl: connector.redirectUrl, + connectionId: connector.connectionId, + }); + }, + { + successMessage: `${connector.type} authorization complete`, + errorMessage: `${connector.type} authorization failed`, + } + ); + + oauthOutcomes.set(connector.type, oauthResult.status); + } + } + } + } + + printSummary(results, oauthOutcomes); + return { outroMessage }; +} + +export function getConnectorsPushCommand(context: CLIContext): Command { + return new Command("push") + .description( + "Push local connectors to Base44 (syncs scopes and removes unlisted)" + ) + .action(async () => { + await runCommand(pushConnectorsAction, { requireAuth: true }, context); + }); +} diff --git a/src/cli/program.ts b/src/cli/program.ts index 8d01d274..034fc486 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -3,6 +3,7 @@ import { getAgentsCommand } from "@/cli/commands/agents/index.js"; import { getLoginCommand } from "@/cli/commands/auth/login.js"; import { getLogoutCommand } from "@/cli/commands/auth/logout.js"; import { getWhoamiCommand } from "@/cli/commands/auth/whoami.js"; +import { getConnectorsCommand } from "@/cli/commands/connectors/index.js"; import { getDashboardCommand } from "@/cli/commands/dashboard/index.js"; import { getEntitiesPushCommand } from "@/cli/commands/entities/push.js"; import { getFunctionsDeployCommand } from "@/cli/commands/functions/deploy.js"; @@ -44,6 +45,9 @@ export function createProgram(context: CLIContext): Command { // Register agents commands program.addCommand(getAgentsCommand(context)); + // Register connectors commands + program.addCommand(getConnectorsCommand(context)); + // Register functions commands program.addCommand(getFunctionsDeployCommand(context)); diff --git a/src/core/project/config.ts b/src/core/project/config.ts index 36738a06..5dc1abd7 100644 --- a/src/core/project/config.ts +++ b/src/core/project/config.ts @@ -5,6 +5,7 @@ import { ConfigNotFoundError, SchemaValidationError } from "@/core/errors.js"; import { ProjectConfigSchema } from "@/core/project/schema.js"; import type { ProjectData, ProjectRoot } from "@/core/project/types.js"; import { agentResource } from "@/core/resources/agent/index.js"; +import { connectorResource } from "@/core/resources/connector/index.js"; import { entityResource } from "@/core/resources/entity/index.js"; import { functionResource } from "@/core/resources/function/index.js"; import { readJsonFile } from "@/core/utils/fs.js"; @@ -91,10 +92,11 @@ export async function readProjectConfig( const project = result.data; const configDir = dirname(configPath); - const [entities, functions, agents] = await Promise.all([ + const [entities, functions, agents, connectors] = await Promise.all([ entityResource.readAll(join(configDir, project.entitiesDir)), functionResource.readAll(join(configDir, project.functionsDir)), agentResource.readAll(join(configDir, project.agentsDir)), + connectorResource.readAll(join(configDir, project.connectorsDir)), ]); return { @@ -102,5 +104,6 @@ export async function readProjectConfig( entities, functions, agents, + connectors, }; } diff --git a/src/core/project/schema.ts b/src/core/project/schema.ts index a69af6f0..287206e1 100644 --- a/src/core/project/schema.ts +++ b/src/core/project/schema.ts @@ -32,6 +32,7 @@ export const ProjectConfigSchema = z.object({ entitiesDir: z.string().optional().default("entities"), functionsDir: z.string().optional().default("functions"), agentsDir: z.string().optional().default("agents"), + connectorsDir: z.string().optional().default("connectors"), }); export type SiteConfig = z.infer; diff --git a/src/core/project/types.ts b/src/core/project/types.ts index c910aa47..f1e8fa5c 100644 --- a/src/core/project/types.ts +++ b/src/core/project/types.ts @@ -1,5 +1,6 @@ import type { ProjectConfig } from "@/core/project/schema.js"; import type { AgentConfig } from "@/core/resources/agent/index.js"; +import type { ConnectorResource } from "@/core/resources/connector/index.js"; import type { Entity } from "@/core/resources/entity/index.js"; import type { BackendFunction } from "@/core/resources/function/index.js"; @@ -18,4 +19,5 @@ export interface ProjectData { entities: Entity[]; functions: BackendFunction[]; agents: AgentConfig[]; + connectors: ConnectorResource[]; } diff --git a/src/core/resources/connector/api.ts b/src/core/resources/connector/api.ts index 9de86e2f..a3dc2ad9 100644 --- a/src/core/resources/connector/api.ts +++ b/src/core/resources/connector/api.ts @@ -6,13 +6,13 @@ import type { ListConnectorsResponse, OAuthStatusResponse, RemoveConnectorResponse, - SyncConnectorResponse, + SetConnectorResponse, } from "./schema.js"; import { ListConnectorsResponseSchema, OAuthStatusResponseSchema, RemoveConnectorResponseSchema, - SyncConnectorResponseSchema, + SetConnectorResponseSchema, } from "./schema.js"; /** @@ -41,25 +41,27 @@ export async function listConnectors(): Promise { return result.data; } -export async function syncConnector( +export async function setConnector( integrationType: IntegrationType, scopes: string[] -): Promise { +): Promise { const appClient = getAppClient(); let response: KyResponse; try { - response = await appClient.post("external-auth/sync", { - json: { - integration_type: integrationType, - scopes, - }, - }); + response = await appClient.put( + `external-auth/integrations/${integrationType}`, + { + json: { + scopes, + }, + } + ); } catch (error) { - throw await ApiError.fromHttpError(error, "syncing connector"); + throw await ApiError.fromHttpError(error, "setting connector"); } - const result = SyncConnectorResponseSchema.safeParse(await response.json()); + const result = SetConnectorResponseSchema.safeParse(await response.json()); if (!result.success) { throw new SchemaValidationError( diff --git a/src/core/resources/connector/config.ts b/src/core/resources/connector/config.ts index 1430ff5e..78cdee84 100644 --- a/src/core/resources/connector/config.ts +++ b/src/core/resources/connector/config.ts @@ -1,14 +1,10 @@ -import { basename } from "node:path"; import { globby } from "globby"; import { SchemaValidationError } from "@/core/errors.js"; import { CONFIG_FILE_EXTENSION_GLOB } from "../../consts.js"; import { pathExists, readJsonFile } from "../../utils/fs.js"; import type { ConnectorResource } from "./schema.js"; -import { ConnectorResourceSchema, IntegrationTypeSchema } from "./schema.js"; +import { ConnectorResourceSchema } from "./schema.js"; -/** - * Read and validate a single connector file. - */ async function readConnectorFile( connectorPath: string ): Promise { @@ -23,24 +19,6 @@ async function readConnectorFile( ); } - // Validate that filename matches the type - const filename = basename(connectorPath).replace(/\.(json|jsonc)$/, ""); - const typeResult = IntegrationTypeSchema.safeParse(filename); - - if (!typeResult.success) { - throw new SchemaValidationError( - `Connector filename "${filename}" is not a valid integration type`, - typeResult.error, - connectorPath - ); - } - - if (filename !== result.data.type) { - throw new Error( - `Connector filename "${filename}" does not match type "${result.data.type}" in ${connectorPath}` - ); - } - return result.data; } @@ -64,7 +42,6 @@ export async function readAllConnectors( files.map((filePath) => readConnectorFile(filePath)) ); - // Check for duplicate types const types = new Set(); for (const connector of connectors) { if (types.has(connector.type)) { diff --git a/src/core/resources/connector/oauth.ts b/src/core/resources/connector/oauth.ts index 0ca0595e..6efd2d37 100644 --- a/src/core/resources/connector/oauth.ts +++ b/src/core/resources/connector/oauth.ts @@ -1,10 +1,10 @@ import open from "open"; import pWaitFor, { TimeoutError } from "p-wait-for"; import { getOAuthStatus } from "./api.js"; -import type { IntegrationType, ConnectorOAuthStatus } from "./schema.js"; +import type { ConnectorOAuthStatus, IntegrationType } from "./schema.js"; const POLL_INTERVAL_MS = 2000; -const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes +const POLL_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes export interface OAuthFlowParams { type: IntegrationType; diff --git a/src/core/resources/connector/push.ts b/src/core/resources/connector/push.ts index 6f2774f0..f386447c 100644 --- a/src/core/resources/connector/push.ts +++ b/src/core/resources/connector/push.ts @@ -1,12 +1,8 @@ -import { - listConnectors, - removeConnector, - syncConnector, -} from "./api.js"; +import { listConnectors, removeConnector, setConnector } from "./api.js"; import type { ConnectorResource, IntegrationType, - SyncConnectorResponse, + SetConnectorResponse, } from "./schema.js"; export interface ConnectorSyncResult { @@ -30,8 +26,8 @@ export async function pushConnectors( for (const connector of connectors) { try { - const response = await syncConnector(connector.type, connector.scopes); - results.push(syncResponseToResult(connector.type, response)); + const response = await setConnector(connector.type, connector.scopes); + results.push(setResponseToResult(connector.type, response)); } catch (err) { results.push({ type: connector.type, @@ -62,15 +58,17 @@ export async function pushConnectors( return { results }; } -function syncResponseToResult( +function setResponseToResult( type: IntegrationType, - response: SyncConnectorResponse + response: SetConnectorResponse ): ConnectorSyncResult { if (response.error === "different_user") { return { type, action: "error", - error: response.error_message || `Already connected by ${response.other_user_email}`, + error: + response.error_message || + `Already connected by ${response.other_user_email}`, }; } diff --git a/src/core/resources/connector/schema.ts b/src/core/resources/connector/schema.ts index 4406d01f..bf9249c0 100644 --- a/src/core/resources/connector/schema.ts +++ b/src/core/resources/connector/schema.ts @@ -107,9 +107,9 @@ export const IntegrationTypeSchema = z.enum([ export type IntegrationType = z.infer; export const ConnectorStatusSchema = z.enum([ - "ACTIVE", - "DISCONNECTED", - "EXPIRED", + "active", + "disconnected", + "expired", ]); export type ConnectorStatus = z.infer; @@ -131,18 +131,22 @@ export type ListConnectorsResponse = z.infer< typeof ListConnectorsResponseSchema >; -export const SyncConnectorResponseSchema = z.object({ +export const SetConnectorResponseSchema = z.object({ redirect_url: z.string().nullable(), connection_id: z.string().nullable(), already_authorized: z.boolean(), - error: z.literal("different_user").optional(), - error_message: z.string().optional(), - other_user_email: z.string().optional(), + error: z.string().nullable().optional(), + error_message: z.string().nullable().optional(), + other_user_email: z.string().nullable().optional(), }); -export type SyncConnectorResponse = z.infer; +export type SetConnectorResponse = z.infer; -export const ConnectorOAuthStatusSchema = z.enum(["ACTIVE", "FAILED", "PENDING"]); +export const ConnectorOAuthStatusSchema = z.enum([ + "ACTIVE", + "FAILED", + "PENDING", +]); export type ConnectorOAuthStatus = z.infer; diff --git a/tests/cli/connectors_push.spec.ts b/tests/cli/connectors_push.spec.ts new file mode 100644 index 00000000..33fa391b --- /dev/null +++ b/tests/cli/connectors_push.spec.ts @@ -0,0 +1,122 @@ +import { describe, it } from "vitest"; +import { fixture, setupCLITests } from "./testkit/index.js"; + +describe("connectors push command", () => { + const t = setupCLITests(); + + it("shows message when no local connectors found", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockConnectorsList({ integrations: [] }); + + const result = await t.run("connectors", "push"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("No local connectors found"); + }); + + it("fails when not in a project directory", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + + const result = await t.run("connectors", "push"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("No Base44 project found"); + }); + + it("finds and lists connectors in project", async () => { + await t.givenLoggedInWithProject(fixture("with-connectors")); + t.api.mockConnectorsList({ integrations: [] }); + t.api.mockConnectorSet({ + redirect_url: null, + connection_id: null, + already_authorized: true, + }); + + const result = await t.run("connectors", "push"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Found 3 connectors to push"); + }); + + it("displays synced connectors with checkmark", async () => { + await t.givenLoggedInWithProject(fixture("with-connectors")); + t.api.mockConnectorsList({ integrations: [] }); + t.api.mockConnectorSet({ + redirect_url: null, + connection_id: null, + already_authorized: true, + }); + + const result = await t.run("connectors", "push"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("googlecalendar"); + t.expectResult(result).toContain("slack"); + t.expectResult(result).toContain("notion"); + }); + + it("displays removed connectors", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockConnectorsList({ + integrations: [ + { integration_type: "slack", status: "ACTIVE", scopes: ["chat:write"] }, + ], + }); + t.api.mockConnectorRemove({ status: "removed", integration_type: "slack" }); + + const result = await t.run("connectors", "push"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("slack"); + t.expectResult(result).toContain("Removed:"); + }); + + it("displays error when sync fails", async () => { + await t.givenLoggedInWithProject(fixture("with-connectors")); + t.api.mockConnectorsList({ integrations: [] }); + t.api.mockConnectorSetError({ + status: 500, + body: { error: "Server error" }, + }); + + const result = await t.run("connectors", "push"); + + // Errors are handled per-connector, command still succeeds + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("googlecalendar"); + }); + + it("shows needs authorization when redirect_url is returned", async () => { + await t.givenLoggedInWithProject(fixture("with-connectors")); + t.api.mockConnectorsList({ integrations: [] }); + t.api.mockConnectorSet({ + redirect_url: "https://accounts.google.com/oauth", + connection_id: "conn_123", + already_authorized: false, + }); + + const result = await t.run("connectors", "push"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("needs authorization"); + t.expectResult(result).toContain("Skipped OAuth in CI"); + }); + + it("shows error for different_user response", async () => { + await t.givenLoggedInWithProject(fixture("with-connectors")); + t.api.mockConnectorsList({ integrations: [] }); + t.api.mockConnectorSet({ + redirect_url: null, + connection_id: null, + already_authorized: false, + error: "different_user", + error_message: "Already connected by another user", + other_user_email: "other@example.com", + }); + + const result = await t.run("connectors", "push"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Already connected by another user"); + }); +}); diff --git a/tests/cli/testkit/Base44APIMock.ts b/tests/cli/testkit/Base44APIMock.ts index c1e1d4ec..8776836c 100644 --- a/tests/cli/testkit/Base44APIMock.ts +++ b/tests/cli/testkit/Base44APIMock.ts @@ -57,6 +57,33 @@ export interface AgentsFetchResponse { total: number; } +export interface ConnectorsListResponse { + integrations: Array<{ + integration_type: string; + status: string; + scopes: string[]; + user_email?: string; + }>; +} + +export interface ConnectorSetResponse { + redirect_url: string | null; + connection_id: string | null; + already_authorized: boolean; + error?: "different_user"; + error_message?: string; + other_user_email?: string; +} + +export interface ConnectorOAuthStatusResponse { + status: "ACTIVE" | "FAILED" | "PENDING"; +} + +export interface ConnectorRemoveResponse { + status: "removed"; + integration_type: string; +} + export interface CreateAppResponse { id: string; name: string; @@ -182,6 +209,50 @@ export class Base44APIMock { return this; } + // ─── CONNECTOR ENDPOINTS ────────────────────────────────── + + /** Mock GET /api/apps/{appId}/external-auth/list - List connectors */ + mockConnectorsList(response: ConnectorsListResponse): this { + this.handlers.push( + http.get(`${BASE_URL}/api/apps/${this.appId}/external-auth/list`, () => + HttpResponse.json(response) + ) + ); + return this; + } + + /** Mock PUT /api/apps/{appId}/external-auth/integrations/{type} - Set connector */ + mockConnectorSet(response: ConnectorSetResponse): this { + this.handlers.push( + http.put( + `${BASE_URL}/api/apps/${this.appId}/external-auth/integrations/:type`, + () => HttpResponse.json(response) + ) + ); + return this; + } + + /** Mock GET /api/apps/{appId}/external-auth/status - Get OAuth status */ + mockConnectorOAuthStatus(response: ConnectorOAuthStatusResponse): this { + this.handlers.push( + http.get(`${BASE_URL}/api/apps/${this.appId}/external-auth/status`, () => + HttpResponse.json(response) + ) + ); + return this; + } + + /** Mock DELETE /api/apps/{appId}/external-auth/integrations/{type}/remove */ + mockConnectorRemove(response: ConnectorRemoveResponse): this { + this.handlers.push( + http.delete( + `${BASE_URL}/api/apps/${this.appId}/external-auth/integrations/:type/remove`, + () => HttpResponse.json(response) + ) + ); + return this; + } + // ─── GENERAL ENDPOINTS ───────────────────────────────────── /** Mock POST /api/apps - Create new app */ @@ -273,6 +344,24 @@ export class Base44APIMock { return this.mockError("get", "/oauth/userinfo", error); } + /** Mock connectors list to return an error */ + mockConnectorsListError(error: ErrorResponse): this { + return this.mockError( + "get", + `/api/apps/${this.appId}/external-auth/list`, + error + ); + } + + /** Mock connector set to return an error */ + mockConnectorSetError(error: ErrorResponse): this { + return this.mockError( + "put", + `/api/apps/${this.appId}/external-auth/integrations/:type`, + error + ); + } + // ─── INTERNAL ────────────────────────────────────────────── /** Apply all registered handlers to MSW (called by CLITestkit.run()) */ diff --git a/tests/core/connectors.spec.ts b/tests/core/connectors.spec.ts index 6ea0d316..8bf6ea9b 100644 --- a/tests/core/connectors.spec.ts +++ b/tests/core/connectors.spec.ts @@ -3,14 +3,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import * as api from "../../src/core/resources/connector/api.js"; import { readAllConnectors } from "../../src/core/resources/connector/config.js"; import { - runOAuthFlow, type OAuthFlowParams, + runOAuthFlow, } from "../../src/core/resources/connector/oauth.js"; import { pushConnectors } from "../../src/core/resources/connector/push.js"; import { + type ConnectorResource, ConnectorResourceSchema, IntegrationTypeSchema, - type ConnectorResource, } from "../../src/core/resources/connector/schema.js"; vi.mock("../../src/core/resources/connector/api.js"); @@ -144,21 +144,10 @@ describe("readAllConnectors", () => { "Invalid connector file" ); }); - - it("throws error when filename does not match type", async () => { - const connectorsDir = resolve( - FIXTURES_DIR, - "connector-type-mismatch/connectors" - ); - - await expect(readAllConnectors(connectorsDir)).rejects.toThrow( - /does not match type/ - ); - }); }); const mockListConnectors = vi.mocked(api.listConnectors); -const mockSyncConnector = vi.mocked(api.syncConnector); +const mockSetConnector = vi.mocked(api.setConnector); const mockRemoveConnector = vi.mocked(api.removeConnector); describe("pushConnectors", () => { @@ -177,7 +166,7 @@ describe("pushConnectors", () => { const local: ConnectorResource[] = [ { type: "gmail", scopes: ["https://mail.google.com/"] }, ]; - mockSyncConnector.mockResolvedValue({ + mockSetConnector.mockResolvedValue({ redirect_url: null, connection_id: null, already_authorized: true, @@ -185,7 +174,7 @@ describe("pushConnectors", () => { const result = await pushConnectors(local); - expect(mockSyncConnector).toHaveBeenCalledWith("gmail", [ + expect(mockSetConnector).toHaveBeenCalledWith("gmail", [ "https://mail.google.com/", ]); expect(result.results).toEqual([{ type: "gmail", action: "synced" }]); @@ -217,7 +206,7 @@ describe("pushConnectors", () => { { integration_type: "slack", status: "ACTIVE", scopes: ["chat:write"] }, ], }); - mockSyncConnector.mockResolvedValue({ + mockSetConnector.mockResolvedValue({ redirect_url: null, connection_id: null, already_authorized: true, @@ -229,7 +218,7 @@ describe("pushConnectors", () => { const result = await pushConnectors(local); - expect(mockSyncConnector).toHaveBeenCalledWith("gmail", [ + expect(mockSetConnector).toHaveBeenCalledWith("gmail", [ "https://mail.google.com/", ]); expect(mockRemoveConnector).toHaveBeenCalledWith("slack"); @@ -252,7 +241,7 @@ describe("pushConnectors", () => { }, ], }); - mockSyncConnector.mockResolvedValue({ + mockSetConnector.mockResolvedValue({ redirect_url: null, connection_id: null, already_authorized: true, @@ -268,7 +257,7 @@ describe("pushConnectors", () => { const local: ConnectorResource[] = [ { type: "gmail", scopes: ["https://mail.google.com/"] }, ]; - mockSyncConnector.mockResolvedValue({ + mockSetConnector.mockResolvedValue({ redirect_url: "https://accounts.google.com/oauth", connection_id: "conn_123", already_authorized: false, @@ -290,7 +279,7 @@ describe("pushConnectors", () => { const local: ConnectorResource[] = [ { type: "gmail", scopes: ["https://mail.google.com/"] }, ]; - mockSyncConnector.mockResolvedValue({ + mockSetConnector.mockResolvedValue({ redirect_url: null, connection_id: null, already_authorized: false, @@ -314,7 +303,7 @@ describe("pushConnectors", () => { const local: ConnectorResource[] = [ { type: "gmail", scopes: ["https://mail.google.com/"] }, ]; - mockSyncConnector.mockRejectedValue(new Error("Network error")); + mockSetConnector.mockRejectedValue(new Error("Network error")); const result = await pushConnectors(local); @@ -343,7 +332,7 @@ describe("pushConnectors", () => { { type: "gmail", scopes: ["https://mail.google.com/"] }, { type: "slack", scopes: ["chat:write"] }, ]; - mockSyncConnector.mockResolvedValue({ + mockSetConnector.mockResolvedValue({ redirect_url: null, connection_id: null, already_authorized: true, @@ -351,7 +340,7 @@ describe("pushConnectors", () => { const result = await pushConnectors(local); - expect(mockSyncConnector).toHaveBeenCalledTimes(2); + expect(mockSetConnector).toHaveBeenCalledTimes(2); expect(result.results).toEqual([ { type: "gmail", action: "synced" }, { type: "slack", action: "synced" }, diff --git a/tests/fixtures/connector-type-mismatch/base44/.app.jsonc b/tests/fixtures/connector-type-mismatch/base44/.app.jsonc deleted file mode 100644 index d7852426..00000000 --- a/tests/fixtures/connector-type-mismatch/base44/.app.jsonc +++ /dev/null @@ -1,4 +0,0 @@ -// Base44 App Configuration -{ - "id": "test-app-id" -} diff --git a/tests/fixtures/connector-type-mismatch/config.jsonc b/tests/fixtures/connector-type-mismatch/config.jsonc deleted file mode 100644 index 0f709d6f..00000000 --- a/tests/fixtures/connector-type-mismatch/config.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "Project with Type Mismatch Connector" -} diff --git a/tests/fixtures/connector-type-mismatch/connectors/googlecalendar.jsonc b/tests/fixtures/connector-type-mismatch/connectors/googlecalendar.jsonc deleted file mode 100644 index fb7389bf..00000000 --- a/tests/fixtures/connector-type-mismatch/connectors/googlecalendar.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -// Type mismatch - filename is googlecalendar but type is slack -{ - "type": "slack", - "scopes": [] -}