mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 19:06:06 +02:00
* feat(vcs): gate self-hosted Git providers to self-host deployments only (MUL-3772) The Forgejo/Gitea/GitLab integration is intended for self-hosted Multica, where Multica can reach a Git instance on the operator's own network. On the managed multi-tenant cloud it adds an SSRF surface (connect validates a user-supplied instance URL from the server) and would store third-party Git tokens for all tenants under one key, while only serving the small subset of users whose instance is publicly reachable. Product decision: offer it on self-host only. - Add an explicit deployment switch MULTICA_VCS_INTEGRATION_ENABLED (default off). Connect, rotate, and webhook now require BOTH the switch on AND a valid MULTICA_VCS_SECRET_KEY — the switch is the product boundary, not key presence alone. When off, connect/rotate return 404 and the webhook returns a bare 404 (no config leak), independent of the frontend. - /api/config exposes vcs_integration_available (mirrors the switch, omitted when false) so the Settings UI hides the whole "Git providers" section on cloud instead of surfacing an operator-only "missing key" hint. - docker-compose.selfhost.yml defaults the switch on; .env.example documents it. - Docs (en/zh) lead with a callout: available on self-hosted Multica only, not Multica Cloud, and clarify "self-hosted" means Multica itself, not just Git. #5006 / #5883 stay in place — the schema and backend capability are retained; this only gates availability. No cloud VCS connection can exist (connect always required the key, which the cloud never set), so nothing needs migrating. Verified: go build/vet + VCS/config handler tests on a fresh migrated DB (incl. a new disabled-deployment 404 test); pnpm typecheck (core + views) and the integrations-tab + core schema/config vitest suites pass. Co-authored-by: multica-agent <github@multica.ai> * fix(vcs): complete self-host integration gating (MUL-5138) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai>
111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { ApiError } from "@multica/core/api";
|
|
import { configStore } from "@multica/core/config";
|
|
import { COMPOSIO_MCP_APPS_FLAG } from "@multica/core/feature-flags";
|
|
import { I18nProvider } from "@multica/core/i18n/react";
|
|
import enCommon from "../../locales/en/common.json";
|
|
import enSettings from "../../locales/en/settings.json";
|
|
|
|
const composioErrorRef = vi.hoisted(() => ({
|
|
current: null as Error | null,
|
|
}));
|
|
const queryCallsRef = vi.hoisted(() => ({
|
|
current: [] as { queryKey: unknown[]; enabled?: boolean }[],
|
|
}));
|
|
|
|
vi.mock("@tanstack/react-query", () => ({
|
|
useQuery: (opts: { queryKey: unknown[]; enabled?: boolean }) => {
|
|
queryCallsRef.current.push(opts);
|
|
return {
|
|
data: undefined,
|
|
error: opts.enabled === false ? null : composioErrorRef.current,
|
|
isError: opts.enabled !== false && composioErrorRef.current != null,
|
|
};
|
|
},
|
|
queryOptions: <T,>(opts: T) => opts,
|
|
}));
|
|
|
|
vi.mock("@multica/core/composio", () => ({
|
|
composioToolkitsOptions: () => ({ queryKey: ["composio", "toolkits"] }),
|
|
}));
|
|
|
|
vi.mock("./lark-tab", () => ({
|
|
LarkTab: () => <div data-testid="lark-tab" />,
|
|
}));
|
|
|
|
vi.mock("./composio-tab", () => ({
|
|
ComposioTab: () => <div data-testid="composio-tab" />,
|
|
}));
|
|
|
|
vi.mock("./slack-tab", () => ({
|
|
SlackTab: () => <div data-testid="slack-tab" />,
|
|
}));
|
|
|
|
vi.mock("./vcs-tab", () => ({
|
|
VCSTab: () => <div data-testid="vcs-tab" />,
|
|
}));
|
|
|
|
import { IntegrationsTab } from "./integrations-tab";
|
|
|
|
function renderTab() {
|
|
return render(
|
|
<I18nProvider locale="en" resources={{ en: { common: enCommon, settings: enSettings } }}>
|
|
<IntegrationsTab />
|
|
</I18nProvider>,
|
|
);
|
|
}
|
|
|
|
describe("Settings IntegrationsTab", () => {
|
|
beforeEach(() => {
|
|
queryCallsRef.current = [];
|
|
composioErrorRef.current = null;
|
|
configStore.getState().setFeatureFlags({ [COMPOSIO_MCP_APPS_FLAG]: true });
|
|
// Reset the self-host-only VCS gate to its default (hidden) so tests stay
|
|
// isolated; individual tests opt in below.
|
|
configStore.getState().setAuthConfig({ allowSignup: true, vcsIntegrationAvailable: false });
|
|
});
|
|
|
|
it("hides Composio and disables the toolkits query when the feature flag is off", () => {
|
|
configStore.getState().setFeatureFlags({ [COMPOSIO_MCP_APPS_FLAG]: false });
|
|
|
|
renderTab();
|
|
|
|
expect(screen.queryByTestId("composio-tab")).toBeNull();
|
|
expect(queryCallsRef.current).toHaveLength(1);
|
|
expect(queryCallsRef.current[0]?.enabled).toBe(false);
|
|
});
|
|
|
|
it("shows Composio when the feature flag is on and the integration is configured", () => {
|
|
renderTab();
|
|
|
|
expect(screen.getByTestId("composio-tab")).toBeInTheDocument();
|
|
expect(queryCallsRef.current[0]?.enabled).toBe(true);
|
|
});
|
|
|
|
it("hides Composio when the feature flag is on but the server reports 503", () => {
|
|
composioErrorRef.current = new ApiError("unavailable", 503, "Service Unavailable");
|
|
|
|
renderTab();
|
|
|
|
expect(screen.queryByTestId("composio-tab")).toBeNull();
|
|
});
|
|
|
|
it("hides the Git providers section when the deployment reports it unavailable", () => {
|
|
// Default (managed cloud / older server): vcsIntegrationAvailable is false.
|
|
renderTab();
|
|
|
|
expect(screen.queryByTestId("vcs-tab")).toBeNull();
|
|
});
|
|
|
|
it("shows the Git providers section on a self-hosted deployment that enables it", () => {
|
|
configStore.getState().setAuthConfig({ allowSignup: true, vcsIntegrationAvailable: true });
|
|
|
|
renderTab();
|
|
|
|
expect(screen.getByTestId("vcs-tab")).toBeInTheDocument();
|
|
});
|
|
});
|