mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-22 19:51:52 -05:00
939fedfca4
Signed-off-by: gulshank0 <gulshanbahadur002@gmail.com> Co-authored-by: Tiago Farto <tiago@formbricks.com> Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Tiago <1585571+xernobyl@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Theodór Tómas <theodortomas@gmail.com> Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Co-authored-by: Bhagya Amarasinghe <b.sithumini@yahoo.com> Co-authored-by: Chowdhury Tafsir Ahmed Siddiki <ctafsiras@gmail.com> Co-authored-by: neila <40727091+neila@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Harsh Bhat <90265455+harshsbhat@users.noreply.github.com> Co-authored-by: Harsh Bhat <harshbhat@Harshs-MacBook-Air.local> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Balázs Úr <balazs@urbalazs.hu> Co-authored-by: Gulshan <gulshanbahadur002@gmail.com> Co-authored-by: Harsh Bhat <harsh121102@gmail.com> Co-authored-by: Javi Aguilar <122741+itsjavi@users.noreply.github.com> Co-authored-by: Johannes <jobenjada@users.noreply.github.com>
328 lines
10 KiB
TypeScript
328 lines
10 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { describe, expect, test, vi } from "vitest";
|
|
import { TAPIKeyWorkspacePermission } from "@formbricks/types/auth";
|
|
import {
|
|
DatabaseError,
|
|
InvalidInputError,
|
|
ResourceNotFoundError,
|
|
UniqueConstraintError,
|
|
} from "@formbricks/types/errors";
|
|
import { getApiKeyWithPermissions } from "@/modules/organization/settings/api-keys/lib/api-key";
|
|
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
|
import { authenticateRequest, handleErrorResponse } from "./auth";
|
|
|
|
vi.mock("@/modules/organization/settings/api-keys/lib/api-key", () => ({
|
|
getApiKeyWithPermissions: vi.fn(),
|
|
}));
|
|
|
|
describe("getApiKeyWithPermissions", () => {
|
|
test("returns API key data with permissions when valid key is provided", async () => {
|
|
const mockApiKeyData = {
|
|
id: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all" as const,
|
|
hashedKey: "hashed-key",
|
|
createdAt: new Date(),
|
|
createdBy: "user-id",
|
|
lastUsedAt: null,
|
|
label: "Test API Key",
|
|
apiKeyWorkspaces: [
|
|
{
|
|
workspaceId: "workspace-1",
|
|
permission: "manage" as const,
|
|
workspace: { id: "workspace-1", name: "Workspace 1" },
|
|
},
|
|
],
|
|
};
|
|
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(mockApiKeyData as any);
|
|
|
|
const result = await getApiKeyWithPermissions("test-api-key");
|
|
|
|
expect(result).toEqual(mockApiKeyData);
|
|
expect(getApiKeyWithPermissions).toHaveBeenCalledWith("test-api-key");
|
|
});
|
|
|
|
test("returns null when API key is not found", async () => {
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(null);
|
|
|
|
const result = await getApiKeyWithPermissions("invalid-key");
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("hasPermission", () => {
|
|
const permissions: TAPIKeyWorkspacePermission[] = [
|
|
{
|
|
permission: "manage",
|
|
workspaceId: "workspace-1",
|
|
workspaceName: "Workspace 1",
|
|
},
|
|
{
|
|
permission: "write",
|
|
workspaceId: "workspace-2",
|
|
workspaceName: "Workspace 2",
|
|
},
|
|
{
|
|
permission: "read",
|
|
workspaceId: "workspace-3",
|
|
workspaceName: "Workspace 3",
|
|
},
|
|
];
|
|
|
|
test("returns true for manage permission with any method", () => {
|
|
expect(hasPermission(permissions, "workspace-1", "GET")).toBe(true);
|
|
expect(hasPermission(permissions, "workspace-1", "POST")).toBe(true);
|
|
expect(hasPermission(permissions, "workspace-1", "DELETE")).toBe(true);
|
|
});
|
|
|
|
test("handles write permission correctly", () => {
|
|
expect(hasPermission(permissions, "workspace-2", "GET")).toBe(true);
|
|
expect(hasPermission(permissions, "workspace-2", "POST")).toBe(true);
|
|
expect(hasPermission(permissions, "workspace-2", "DELETE")).toBe(false);
|
|
});
|
|
|
|
test("handles read permission correctly", () => {
|
|
expect(hasPermission(permissions, "workspace-3", "GET")).toBe(true);
|
|
expect(hasPermission(permissions, "workspace-3", "POST")).toBe(false);
|
|
expect(hasPermission(permissions, "workspace-3", "DELETE")).toBe(false);
|
|
});
|
|
|
|
test("returns false for non-existent workspace", () => {
|
|
expect(hasPermission(permissions, "workspace-4", "GET")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("authenticateRequest", () => {
|
|
test("should return authentication data for valid API key", async () => {
|
|
const request = new NextRequest("http://localhost", {
|
|
headers: { "x-api-key": "valid-api-key" },
|
|
});
|
|
|
|
const mockApiKeyData = {
|
|
id: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all" as const,
|
|
createdAt: new Date(),
|
|
createdBy: "user-id",
|
|
lastUsedAt: null,
|
|
label: "Test API Key",
|
|
apiKeyWorkspaces: [
|
|
{
|
|
workspaceId: "workspace-1",
|
|
permission: "manage" as const,
|
|
workspace: { id: "workspace-1", name: "Workspace 1" },
|
|
},
|
|
],
|
|
};
|
|
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(mockApiKeyData as any);
|
|
const result = await authenticateRequest(request);
|
|
|
|
expect(result).toEqual({
|
|
type: "apiKey",
|
|
workspacePermissions: [
|
|
{
|
|
permission: "manage",
|
|
workspaceId: "workspace-1",
|
|
workspaceName: "Workspace 1",
|
|
},
|
|
],
|
|
apiKeyId: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all",
|
|
});
|
|
expect(getApiKeyWithPermissions).toHaveBeenCalledWith("valid-api-key");
|
|
});
|
|
|
|
test("returns null when no API key is provided", async () => {
|
|
const request = new NextRequest("http://localhost");
|
|
const result = await authenticateRequest(request);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("returns null when API key is invalid", async () => {
|
|
const request = new NextRequest("http://localhost", {
|
|
headers: { "x-api-key": "invalid-api-key" },
|
|
});
|
|
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(null);
|
|
|
|
const result = await authenticateRequest(request);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("returns authentication data when API key has no workspace permissions", async () => {
|
|
const request = new NextRequest("http://localhost", {
|
|
headers: { "x-api-key": "valid-api-key" },
|
|
});
|
|
|
|
const mockApiKeyData = {
|
|
id: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all" as const,
|
|
createdAt: new Date(),
|
|
createdBy: "user-id",
|
|
lastUsedAt: null,
|
|
label: "Test API Key",
|
|
apiKeyWorkspaces: [],
|
|
};
|
|
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(mockApiKeyData as any);
|
|
|
|
const result = await authenticateRequest(request, { allowOrganizationOnlyApiKey: true });
|
|
expect(result).toEqual({
|
|
type: "apiKey",
|
|
workspacePermissions: [],
|
|
apiKeyId: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all",
|
|
});
|
|
});
|
|
|
|
test("returns authentication data for bearer API keys", async () => {
|
|
const request = new NextRequest("http://localhost", {
|
|
headers: { authorization: "Bearer fbk_valid_bearer_key" },
|
|
});
|
|
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue({
|
|
id: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all" as const,
|
|
createdAt: new Date(),
|
|
createdBy: "user-id",
|
|
lastUsedAt: null,
|
|
label: "Test API Key",
|
|
apiKeyWorkspaces: [],
|
|
} as any);
|
|
|
|
const result = await authenticateRequest(request, { allowOrganizationOnlyApiKey: true });
|
|
|
|
expect(result).toEqual({
|
|
type: "apiKey",
|
|
workspacePermissions: [],
|
|
apiKeyId: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all",
|
|
});
|
|
expect(getApiKeyWithPermissions).toHaveBeenCalledWith("fbk_valid_bearer_key");
|
|
});
|
|
|
|
test("authenticates a valid API key with no environment permissions when explicitly allowed", async () => {
|
|
const request = new NextRequest("http://localhost", {
|
|
headers: { "x-api-key": "valid-api-key" },
|
|
});
|
|
|
|
const mockApiKeyData = {
|
|
id: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all" as const,
|
|
createdAt: new Date(),
|
|
createdBy: "user-id",
|
|
lastUsedAt: null,
|
|
label: "Test API Key",
|
|
apiKeyWorkspaces: [],
|
|
};
|
|
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(mockApiKeyData as any);
|
|
|
|
const result = await authenticateRequest(request, { allowOrganizationOnlyApiKey: true });
|
|
expect(result).toEqual({
|
|
type: "apiKey",
|
|
workspacePermissions: [],
|
|
apiKeyId: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: "all",
|
|
});
|
|
});
|
|
|
|
test("authenticates a read-only organization API key with no environment permissions", async () => {
|
|
const request = new NextRequest("http://localhost/api/v1/management/surveys", {
|
|
headers: { "x-api-key": "read-only-org-api-key" },
|
|
});
|
|
|
|
const mockApiKeyData = {
|
|
id: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: {
|
|
accessControl: {
|
|
read: true,
|
|
write: false,
|
|
},
|
|
},
|
|
createdAt: new Date(),
|
|
createdBy: "user-id",
|
|
lastUsedAt: null,
|
|
label: "Read-only Organization API Key",
|
|
apiKeyWorkspaces: [],
|
|
};
|
|
|
|
vi.mocked(getApiKeyWithPermissions).mockResolvedValue(mockApiKeyData as any);
|
|
|
|
const result = await authenticateRequest(request, { allowOrganizationOnlyApiKey: true });
|
|
expect(result).toEqual({
|
|
type: "apiKey",
|
|
workspacePermissions: [],
|
|
apiKeyId: "api-key-id",
|
|
organizationId: "org-id",
|
|
organizationAccess: {
|
|
accessControl: {
|
|
read: true,
|
|
write: false,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("handleErrorResponse", () => {
|
|
test("returns 401 notAuthenticated for 'NotAuthenticated' message", async () => {
|
|
const response = handleErrorResponse(new Error("NotAuthenticated"));
|
|
expect(response.status).toBe(401);
|
|
const body = await response.json();
|
|
expect(body.code).toBe("not_authenticated");
|
|
});
|
|
|
|
test("returns 401 unauthorized for 'Unauthorized' message", async () => {
|
|
const response = handleErrorResponse(new Error("Unauthorized"));
|
|
expect(response.status).toBe(401);
|
|
const body = await response.json();
|
|
expect(body.code).toBe("unauthorized");
|
|
});
|
|
|
|
test("returns 409 conflict for UniqueConstraintError", async () => {
|
|
const response = handleErrorResponse(new UniqueConstraintError("Action with name foo already exists"));
|
|
expect(response.status).toBe(409);
|
|
const body = await response.json();
|
|
expect(body.code).toBe("conflict");
|
|
expect(body.message).toBe("Action with name foo already exists");
|
|
});
|
|
|
|
test("returns 400 badRequest for DatabaseError", async () => {
|
|
const response = handleErrorResponse(new DatabaseError("db boom"));
|
|
expect(response.status).toBe(400);
|
|
const body = await response.json();
|
|
expect(body.message).toBe("db boom");
|
|
});
|
|
|
|
test("returns 400 badRequest for InvalidInputError", async () => {
|
|
const response = handleErrorResponse(new InvalidInputError("bad input"));
|
|
expect(response.status).toBe(400);
|
|
const body = await response.json();
|
|
expect(body.message).toBe("bad input");
|
|
});
|
|
|
|
test("returns 400 badRequest for ResourceNotFoundError", async () => {
|
|
const response = handleErrorResponse(new ResourceNotFoundError("Survey", "id-1"));
|
|
expect(response.status).toBe(400);
|
|
});
|
|
|
|
test("returns 500 internalServerError for unknown errors", async () => {
|
|
const response = handleErrorResponse(new Error("something else"));
|
|
expect(response.status).toBe(500);
|
|
const body = await response.json();
|
|
expect(body.message).toBe("Some error occurred");
|
|
});
|
|
});
|