mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-13 03:16:58 -05:00
fix: address gateway review follow-ups
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
const { mockCreateGatewayServiceTokenResponse, mockWithV3ApiWrapper, mockWrapperAuthentication } =
|
||||
vi.hoisted(() => ({
|
||||
mockCreateGatewayServiceTokenResponse: vi.fn(),
|
||||
mockWithV3ApiWrapper: vi.fn(),
|
||||
mockWrapperAuthentication: {
|
||||
current: {
|
||||
user: { id: "user_1" },
|
||||
} as { user: { id: string } } | null,
|
||||
},
|
||||
}));
|
||||
|
||||
const installWrapperMock = () => {
|
||||
mockWithV3ApiWrapper.mockImplementation(
|
||||
({
|
||||
handler,
|
||||
}: {
|
||||
handler: (params: { authentication: { user: { id: string } } | null }) => Promise<Response>;
|
||||
}) =>
|
||||
async () =>
|
||||
await handler({
|
||||
authentication: mockWrapperAuthentication.current,
|
||||
} as never)
|
||||
);
|
||||
};
|
||||
|
||||
vi.mock("@/modules/gateway-auth/lib/token", () => ({
|
||||
createGatewayServiceTokenResponse: mockCreateGatewayServiceTokenResponse,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/api/v3/lib/api-wrapper", () => ({
|
||||
withV3ApiWrapper: mockWithV3ApiWrapper,
|
||||
}));
|
||||
|
||||
describe("POST /api/v3/feedbackRecords/token", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
installWrapperMock();
|
||||
mockWrapperAuthentication.current = {
|
||||
user: { id: "user_1" },
|
||||
};
|
||||
mockCreateGatewayServiceTokenResponse.mockReturnValue(
|
||||
Response.json({
|
||||
token: "gateway-token",
|
||||
expiresAt: "2026-04-24T00:10:00.000Z",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("delegates to the generic gateway token helper for feedbackRecords", async () => {
|
||||
const { POST } = await import("./route");
|
||||
const response = await POST({} as never, {} as never);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toEqual({
|
||||
token: "gateway-token",
|
||||
expiresAt: "2026-04-24T00:10:00.000Z",
|
||||
});
|
||||
expect(mockCreateGatewayServiceTokenResponse).toHaveBeenCalledWith(
|
||||
{ user: { id: "user_1" } },
|
||||
"feedbackRecords"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,6 @@ import { ENCRYPTION_KEY, NEXTAUTH_SECRET } from "@/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto";
|
||||
import { getGatewayAuthServiceTokenPurpose, TGatewayAuthService } from "@/modules/gateway-auth/lib/service";
|
||||
|
||||
export const FEEDBACK_RECORDS_GATEWAY_TOKEN_PURPOSE =
|
||||
getGatewayAuthServiceTokenPurpose("feedbackRecords");
|
||||
const FEEDBACK_RECORDS_GATEWAY_TOKEN_TTL_SECONDS = 60 * 10;
|
||||
|
||||
// Helper function to decrypt with fallback to plain text
|
||||
|
||||
+17
-17
@@ -1,6 +1,6 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { DELETE, GET, HEAD, OPTIONS, PATCH, POST } from "./route";
|
||||
import { authorizeEnvoyRequest } from "./service";
|
||||
|
||||
const {
|
||||
mockAuthenticateApiKeyFromHeaders,
|
||||
@@ -91,7 +91,7 @@ const createRequest = (
|
||||
body,
|
||||
});
|
||||
|
||||
describe("Envoy auth route", () => {
|
||||
describe("authorizeEnvoyRequest", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockGetApiKeyFromHeaders.mockReturnValue(null);
|
||||
@@ -131,7 +131,7 @@ describe("Envoy auth route", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest("http://localhost/api/envoy-auth/api/v3/feedbackRecords", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -160,7 +160,7 @@ describe("Envoy auth route", () => {
|
||||
feedbackRecordDirectoryPermissions: [],
|
||||
});
|
||||
|
||||
const response = await GET(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest("http://localhost/api/envoy-auth/v1/feedback-records", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -173,7 +173,7 @@ describe("Envoy auth route", () => {
|
||||
});
|
||||
|
||||
test("returns 400 for unsupported envoy auth routes", async () => {
|
||||
const response = await GET(createRequest("http://localhost/api/envoy-auth/api/v1/test"));
|
||||
const response = await authorizeEnvoyRequest(createRequest("http://localhost/api/envoy-auth/api/v1/test"));
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
@@ -197,7 +197,7 @@ describe("Envoy auth route", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const response = await GET(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(`http://localhost/api/envoy-auth/v1/feedback-records/${feedbackRecordId}`, {
|
||||
headers: {
|
||||
"x-api-key": "fbk_test",
|
||||
@@ -227,7 +227,7 @@ describe("Envoy auth route", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const response = await GET(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(`http://localhost/api/envoy-auth/v1/feedback-records/${feedbackRecordId}`, {
|
||||
headers: {
|
||||
"x-api-key": "fbk_test",
|
||||
@@ -244,7 +244,7 @@ describe("Envoy auth route", () => {
|
||||
userId: "user_1",
|
||||
});
|
||||
|
||||
const response = await GET(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(
|
||||
`http://localhost/api/envoy-auth/v1/feedback-records?tenant_id=${feedbackRecordDirectoryId}`,
|
||||
{
|
||||
@@ -264,7 +264,7 @@ describe("Envoy auth route", () => {
|
||||
mockGetBearerTokenFromHeaders.mockReturnValue("header.payload.signature");
|
||||
mockVerifyFeedbackRecordsGatewayToken.mockReturnValue({ userId: "user_1" });
|
||||
|
||||
const response = await PATCH(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(`http://localhost/api/envoy-auth/v1/feedback-records/${feedbackRecordId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
@@ -296,7 +296,7 @@ describe("Envoy auth route", () => {
|
||||
userId: "user_2",
|
||||
});
|
||||
|
||||
const response = await GET(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(
|
||||
`http://localhost/api/envoy-auth/v1/feedback-records?tenant_id=${feedbackRecordDirectoryId}`,
|
||||
{
|
||||
@@ -336,7 +336,7 @@ describe("Envoy auth route", () => {
|
||||
feedbackRecordDirectoryPermissions: [],
|
||||
});
|
||||
|
||||
const response = await DELETE(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(
|
||||
`http://localhost/api/envoy-auth/v1/feedback-records?tenant_id=${feedbackRecordDirectoryId}`,
|
||||
{
|
||||
@@ -360,7 +360,7 @@ describe("Envoy auth route", () => {
|
||||
isArchived: true,
|
||||
});
|
||||
|
||||
const response = await GET(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(
|
||||
`http://localhost/api/envoy-auth/v1/feedback-records?tenant_id=${feedbackRecordDirectoryId}`,
|
||||
{
|
||||
@@ -385,7 +385,7 @@ describe("Envoy auth route", () => {
|
||||
feedbackRecordDirectoryPermissions: [],
|
||||
});
|
||||
|
||||
const response = await GET(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(
|
||||
`http://localhost/api/envoy-auth/api/v3/feedbackRecordsFoo?tenant_id=${feedbackRecordDirectoryId}`,
|
||||
{
|
||||
@@ -399,7 +399,7 @@ describe("Envoy auth route", () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
test("handles HEAD requests through the generic route instead of 405ing at Next.js", async () => {
|
||||
test("handles HEAD requests through the generic service instead of 405ing at Next.js", async () => {
|
||||
mockGetApiKeyFromHeaders.mockReturnValue("fbk_test");
|
||||
mockAuthenticateApiKeyFromHeaders.mockResolvedValue({
|
||||
type: "apiKey",
|
||||
@@ -410,7 +410,7 @@ describe("Envoy auth route", () => {
|
||||
feedbackRecordDirectoryPermissions: [],
|
||||
});
|
||||
|
||||
const response = await HEAD(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest(`http://localhost/api/envoy-auth/v1/feedback-records/${feedbackRecordId}`, {
|
||||
method: "HEAD",
|
||||
headers: {
|
||||
@@ -422,7 +422,7 @@ describe("Envoy auth route", () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
test("handles OPTIONS requests through the generic route instead of 405ing at Next.js", async () => {
|
||||
test("handles OPTIONS requests through the generic service instead of 405ing at Next.js", async () => {
|
||||
mockGetApiKeyFromHeaders.mockReturnValue("fbk_test");
|
||||
mockAuthenticateApiKeyFromHeaders.mockResolvedValue({
|
||||
type: "apiKey",
|
||||
@@ -433,7 +433,7 @@ describe("Envoy auth route", () => {
|
||||
feedbackRecordDirectoryPermissions: [],
|
||||
});
|
||||
|
||||
const response = await OPTIONS(
|
||||
const response = await authorizeEnvoyRequest(
|
||||
createRequest("http://localhost/api/envoy-auth/v1/feedback-records", {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
@@ -232,7 +232,7 @@ describe("hub service", () => {
|
||||
expect(vi.mocked(cache.withCache)).toHaveBeenCalledOnce();
|
||||
expect(vi.mocked(cache.withCache)).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
createCacheKey.custom("hub", "0194d8a0-3d55-7ff4-9f62-8d02c3fbcfe8", "feedback_record_tenant"),
|
||||
createCacheKey.hub.feedbackRecordTenant("0194d8a0-3d55-7ff4-9f62-8d02c3fbcfe8"),
|
||||
60_000
|
||||
);
|
||||
expect(retrieve).toHaveBeenCalledWith("0194d8a0-3d55-7ff4-9f62-8d02c3fbcfe8");
|
||||
|
||||
@@ -91,7 +91,7 @@ export const getFeedbackRecordTenant = async (recordId: string): Promise<Feedbac
|
||||
const feedbackRecord = await client.feedbackRecords.retrieve(recordId);
|
||||
return { tenantId: feedbackRecord.tenant_id };
|
||||
},
|
||||
createCacheKey.custom("hub", recordId, "feedback_record_tenant"),
|
||||
createCacheKey.hub.feedbackRecordTenant(recordId),
|
||||
60_000
|
||||
);
|
||||
|
||||
|
||||
+16
@@ -103,6 +103,19 @@ describe("@formbricks/cache cacheKeys", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("hub namespace", () => {
|
||||
test("should create feedback record tenant key", () => {
|
||||
const key = createCacheKey.hub.feedbackRecordTenant("0194d8a0-3d55-7ff4-9f62-8d02c3fbcfe8");
|
||||
expect(key).toBe("fb:hub:0194d8a0-3d55-7ff4-9f62-8d02c3fbcfe8:feedback_record_tenant");
|
||||
});
|
||||
|
||||
test("should throw error for empty feedback record id", () => {
|
||||
expect(() => createCacheKey.hub.feedbackRecordTenant("")).toThrow(
|
||||
"Invalid Cache key: Parts cannot be empty"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom namespace", () => {
|
||||
test("should create custom key with subResource", () => {
|
||||
const key = createCacheKey.custom("analytics", "user-456", "daily-stats");
|
||||
@@ -162,6 +175,7 @@ describe("@formbricks/cache cacheKeys", () => {
|
||||
createCacheKey.organization.billing("org-1"),
|
||||
createCacheKey.license.status("org-1"),
|
||||
createCacheKey.license.previous_result("org-1"),
|
||||
createCacheKey.hub.feedbackRecordTenant("record-1"),
|
||||
createCacheKey.rateLimit.core("api", "user-1", 123456),
|
||||
createCacheKey.custom("analytics", "temp-1"),
|
||||
createCacheKey.custom("analytics", "temp-1", "sub"),
|
||||
@@ -181,6 +195,7 @@ describe("@formbricks/cache cacheKeys", () => {
|
||||
createCacheKey.workspace.state("env-123"),
|
||||
createCacheKey.organization.billing("org-456"),
|
||||
createCacheKey.license.status("license-789"),
|
||||
createCacheKey.hub.feedbackRecordTenant("record-321"),
|
||||
createCacheKey.rateLimit.core("api", "user-101", 1640995200),
|
||||
createCacheKey.custom("analytics", "analytics-102", "daily"),
|
||||
];
|
||||
@@ -198,6 +213,7 @@ describe("@formbricks/cache cacheKeys", () => {
|
||||
expect(() => createCacheKey.workspace.state("")).toThrow(errorMessage);
|
||||
expect(() => createCacheKey.organization.billing("")).toThrow(errorMessage);
|
||||
expect(() => createCacheKey.license.status("")).toThrow(errorMessage);
|
||||
expect(() => createCacheKey.hub.feedbackRecordTenant("")).toThrow(errorMessage);
|
||||
expect(() => createCacheKey.rateLimit.core("", "user", 123)).toThrow(errorMessage);
|
||||
expect(() => createCacheKey.custom("analytics", "")).toThrow(errorMessage);
|
||||
});
|
||||
|
||||
Vendored
+6
@@ -40,6 +40,12 @@ export const createCacheKey = {
|
||||
countBySurveyId: (surveyId: string): CacheKey => makeCacheKey("response", surveyId, "count"),
|
||||
},
|
||||
|
||||
// Hub-related keys
|
||||
hub: {
|
||||
feedbackRecordTenant: (recordId: string): CacheKey =>
|
||||
makeCacheKey("hub", recordId, "feedback_record_tenant"),
|
||||
},
|
||||
|
||||
// Rate limiting and security
|
||||
rateLimit: {
|
||||
core: (namespace: string, identifier: string, windowStart: number): CacheKey =>
|
||||
|
||||
Vendored
+1
-1
@@ -16,4 +16,4 @@ export type CacheKey = z.infer<typeof ZCacheKey>;
|
||||
* Possible namespaces for custom cache keys
|
||||
* Add new namespaces here as they are introduced
|
||||
*/
|
||||
export type CustomCacheNamespace = "analytics" | "billing" | "hub";
|
||||
export type CustomCacheNamespace = "analytics" | "billing";
|
||||
|
||||
Reference in New Issue
Block a user