fix: address gateway review follow-ups

This commit is contained in:
Bhagya Amarasinghe
2026-04-29 12:27:41 +05:30
parent 79d618f77c
commit 3bac488a29
8 changed files with 42 additions and 88 deletions
@@ -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"
);
});
});
-2
View File
@@ -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
@@ -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: {
+1 -1
View File
@@ -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");
+1 -1
View File
@@ -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
View File
@@ -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);
});
+6
View File
@@ -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 =>
+1 -1
View File
@@ -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";