236 lines
7.1 KiB
TypeScript
236 lines
7.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
|
|
|
import { appByIdHandler } from "./appById.handler";
|
|
import type { TAppByIdInputSchema } from "./appById.schema";
|
|
|
|
// Mock the dependencies
|
|
vi.mock("@calcom/app-store/delegationCredential", () => ({
|
|
getUsersCredentialsIncludeServiceAccountKey: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@calcom/app-store/utils", () => ({
|
|
default: vi.fn(),
|
|
sanitizeAppForViewer: vi.fn((app) => {
|
|
const { key: _, credential: _1, credentials: _2, ...sanitized } = app;
|
|
return sanitized;
|
|
}),
|
|
}));
|
|
|
|
import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/app-store/delegationCredential";
|
|
import getApps, { sanitizeAppForViewer } from "@calcom/app-store/utils";
|
|
|
|
import type { CredentialDataWithTeamName, LocationOption } from "@calcom/app-store/utils";
|
|
import type { App } from "@calcom/types/App";
|
|
|
|
describe("appByIdHandler", () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
email: "test@example.com",
|
|
username: "testuser",
|
|
name: "Test User",
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("should not expose key field for globally installed apps", async () => {
|
|
const secretApiKey = "secret-daily-api-key-12345";
|
|
const mockCredential: CredentialDataWithTeamName = {
|
|
id: 0,
|
|
type: "daily_video",
|
|
key: { apikey: secretApiKey },
|
|
userId: 0,
|
|
user: { email: "" },
|
|
teamId: null,
|
|
appId: "daily-video",
|
|
invalid: false,
|
|
delegatedTo: null,
|
|
delegatedToId: null,
|
|
delegationCredentialId: null,
|
|
team: {
|
|
name: "Default",
|
|
},
|
|
};
|
|
|
|
const mockApp: App & {
|
|
credential: CredentialDataWithTeamName | null;
|
|
credentials: CredentialDataWithTeamName[];
|
|
locationOption: LocationOption | null;
|
|
} = {
|
|
type: "daily_video",
|
|
name: "Cal Video",
|
|
description: "Video conferencing",
|
|
variant: "conferencing",
|
|
slug: "daily-video",
|
|
categories: ["conferencing"],
|
|
logo: "icon.svg",
|
|
publisher: "Cal.com",
|
|
url: "https://daily.co",
|
|
email: "help@cal.com",
|
|
isGlobal: true,
|
|
// This is the sensitive key that should NOT be exposed
|
|
key: { apikey: secretApiKey },
|
|
credential: mockCredential,
|
|
credentials: [mockCredential],
|
|
locationOption: {
|
|
value: "integrations:daily",
|
|
label: "Cal Video",
|
|
},
|
|
appData: {
|
|
location: {
|
|
linkType: "dynamic",
|
|
type: "integrations:daily",
|
|
label: "Cal Video",
|
|
},
|
|
},
|
|
};
|
|
|
|
vi.mocked(getUsersCredentialsIncludeServiceAccountKey).mockResolvedValue([]);
|
|
vi.mocked(getApps).mockReturnValue([mockApp]);
|
|
|
|
const result = await appByIdHandler({
|
|
ctx: { user: mockUser },
|
|
input: { appId: "daily-video" } as TAppByIdInputSchema,
|
|
});
|
|
|
|
// Verify that key is NOT in the response
|
|
expect(result).not.toHaveProperty("key");
|
|
expect(result).not.toHaveProperty("credential");
|
|
expect(result).not.toHaveProperty("credentials");
|
|
|
|
// Verify that other properties are preserved
|
|
expect(result).toHaveProperty("slug", "daily-video");
|
|
expect(result).toHaveProperty("name", "Cal Video");
|
|
expect(result).toHaveProperty("isGlobal", true);
|
|
expect(result).toHaveProperty("locationOption");
|
|
expect(result).toHaveProperty("isInstalled", 1);
|
|
|
|
// Verify sanitizeAppForViewer was called
|
|
expect(sanitizeAppForViewer).toHaveBeenCalledWith(mockApp);
|
|
});
|
|
|
|
it("should throw BAD_REQUEST when app is not found", async () => {
|
|
vi.mocked(getUsersCredentialsIncludeServiceAccountKey).mockResolvedValue([]);
|
|
vi.mocked(getApps).mockReturnValue([]);
|
|
|
|
await expect(
|
|
appByIdHandler({
|
|
ctx: { user: mockUser },
|
|
input: { appId: "non-existent-app" } as TAppByIdInputSchema,
|
|
})
|
|
).rejects.toThrow(TRPCError);
|
|
|
|
await expect(
|
|
appByIdHandler({
|
|
ctx: { user: mockUser },
|
|
input: { appId: "non-existent-app" } as TAppByIdInputSchema,
|
|
})
|
|
).rejects.toThrow("Could not find app non-existent-app");
|
|
});
|
|
|
|
it("should preserve all non-sensitive properties", async () => {
|
|
const mockApp: App & {
|
|
credential: CredentialDataWithTeamName | null;
|
|
credentials: CredentialDataWithTeamName[];
|
|
locationOption: LocationOption | null;
|
|
} = {
|
|
type: "zoom_video",
|
|
name: "Zoom",
|
|
description: "Video conferencing",
|
|
variant: "conferencing",
|
|
slug: "zoom",
|
|
categories: ["conferencing"],
|
|
logo: "icon.svg",
|
|
publisher: "Zoom",
|
|
url: "https://zoom.us",
|
|
email: "support@zoom.us",
|
|
isGlobal: false,
|
|
verified: true,
|
|
trending: true,
|
|
rating: 4.5,
|
|
reviews: 1000,
|
|
key: { api_key: "secret-zoom-key" },
|
|
credential: null,
|
|
credentials: [],
|
|
locationOption: null,
|
|
};
|
|
|
|
vi.mocked(getUsersCredentialsIncludeServiceAccountKey).mockResolvedValue([]);
|
|
vi.mocked(getApps).mockReturnValue([mockApp]);
|
|
|
|
const result = await appByIdHandler({
|
|
ctx: { user: mockUser },
|
|
input: { appId: "zoom" } as TAppByIdInputSchema,
|
|
});
|
|
|
|
// Verify sensitive fields are removed
|
|
expect(result).not.toHaveProperty("key");
|
|
expect(result).not.toHaveProperty("credential");
|
|
expect(result).not.toHaveProperty("credentials");
|
|
|
|
// Verify non-sensitive fields are preserved
|
|
expect(result).toHaveProperty("slug", "zoom");
|
|
expect(result).toHaveProperty("name", "Zoom");
|
|
expect(result).toHaveProperty("verified", true);
|
|
expect(result).toHaveProperty("trending", true);
|
|
expect(result).toHaveProperty("rating", 4.5);
|
|
expect(result).toHaveProperty("reviews", 1000);
|
|
expect(result).toHaveProperty("isInstalled", 0);
|
|
});
|
|
|
|
it("should correctly set isInstalled based on credentials length", async () => {
|
|
const mockCredential: CredentialDataWithTeamName = {
|
|
id: 1,
|
|
type: "google_calendar",
|
|
key: { access_token: "token" },
|
|
userId: 1,
|
|
user: { email: "test@example.com" },
|
|
teamId: null,
|
|
appId: "google-calendar",
|
|
invalid: false,
|
|
delegatedTo: null,
|
|
delegatedToId: null,
|
|
delegationCredentialId: null,
|
|
team: null,
|
|
};
|
|
|
|
const mockApp: App & {
|
|
credential: CredentialDataWithTeamName | null;
|
|
credentials: CredentialDataWithTeamName[];
|
|
locationOption: LocationOption | null;
|
|
} = {
|
|
type: "google_calendar",
|
|
name: "Google Calendar",
|
|
description: "Calendar integration",
|
|
variant: "calendar",
|
|
slug: "google-calendar",
|
|
categories: ["calendar"],
|
|
logo: "icon.svg",
|
|
publisher: "Google",
|
|
url: "https://google.com",
|
|
email: "support@google.com",
|
|
key: { api_key: "secret-key" },
|
|
credential: mockCredential,
|
|
credentials: [mockCredential],
|
|
locationOption: null,
|
|
};
|
|
|
|
vi.mocked(getUsersCredentialsIncludeServiceAccountKey).mockResolvedValue([]);
|
|
vi.mocked(getApps).mockReturnValue([mockApp]);
|
|
|
|
const result = await appByIdHandler({
|
|
ctx: { user: mockUser },
|
|
input: { appId: "google-calendar" } as TAppByIdInputSchema,
|
|
});
|
|
|
|
expect(result.isInstalled).toBe(1);
|
|
expect(result).not.toHaveProperty("key");
|
|
});
|
|
});
|
|
|