This commit is contained in:
pandeymangg
2025-08-20 17:28:05 +05:30
parent a362455878
commit 9cff5457d6
16 changed files with 814 additions and 93 deletions

View File

@@ -747,6 +747,7 @@
"api_key_label": "API-Schlüssel Label",
"api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.",
"api_key_updated": "API-Schlüssel aktualisiert",
"delete_permission": "Berechtigung löschen",
"duplicate_access": "Doppelter Projektzugriff nicht erlaubt",
"no_api_keys_yet": "Du hast noch keine API-Schlüssel",
"no_env_permissions_found": "Keine Umgebungsberechtigungen gefunden",

View File

@@ -747,6 +747,7 @@
"api_key_label": "API Key Label",
"api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.",
"api_key_updated": "API Key updated",
"delete_permission": "Delete permission",
"duplicate_access": "Duplicate project access not allowed",
"no_api_keys_yet": "You don't have any API keys yet",
"no_env_permissions_found": "No environment permissions found",

View File

@@ -747,6 +747,7 @@
"api_key_label": "Étiquette de clé API",
"api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.",
"api_key_updated": "Clé API mise à jour",
"delete_permission": "Supprimer une permission",
"duplicate_access": "L'accès en double au projet n'est pas autorisé",
"no_api_keys_yet": "Vous n'avez pas encore de clés API.",
"no_env_permissions_found": "Aucune autorisation d'environnement trouvée",

View File

@@ -747,6 +747,7 @@
"api_key_label": "Rótulo da Chave API",
"api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave de API atualizada",
"delete_permission": "Remover permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Você ainda não tem nenhuma chave de API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",

View File

@@ -747,6 +747,7 @@
"api_key_label": "Etiqueta da Chave API",
"api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
"api_key_updated": "Chave API atualizada",
"delete_permission": "Eliminar permissão",
"duplicate_access": "Acesso duplicado ao projeto não permitido",
"no_api_keys_yet": "Ainda não tem nenhuma chave API",
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",

View File

@@ -747,6 +747,7 @@
"api_key_label": "Etichetă Cheie API",
"api_key_security_warning": "Din motive de securitate, cheia API va fi afișată o singură dată după creare. Vă rugăm să o copiați imediat la destinație.",
"api_key_updated": "Cheie API actualizată",
"delete_permission": "Șterge permisiunea",
"duplicate_access": "Accesul dublu la proiect nu este permis",
"no_api_keys_yet": "Nu aveți încă chei API",
"no_env_permissions_found": "Nu s-au găsit permisiuni pentru mediu",

View File

@@ -747,6 +747,7 @@
"api_key_label": "API 金鑰標籤",
"api_key_security_warning": "為安全起見API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。",
"api_key_updated": "API 金鑰已更新",
"delete_permission": "刪除 權限",
"duplicate_access": "不允許重複的 project 存取",
"no_api_keys_yet": "您還沒有任何 API 金鑰",
"no_env_permissions_found": "找不到環境權限",

View File

@@ -32,6 +32,7 @@
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json",
"build": "tsc && vite build",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"go": "vite build --watch --mode dev"
},
"author": "Formbricks <hola@formbricks.com>",

View File

@@ -0,0 +1,205 @@
import { S3Client, type S3ClientConfig } from "@aws-sdk/client-s3";
import { beforeEach, describe, expect, test, vi } from "vitest";
// Mock the AWS SDK S3Client
vi.mock("@aws-sdk/client-s3", () => ({
S3Client: vi.fn().mockImplementation((config: S3ClientConfig) => ({
config,
send: vi.fn(),
})),
}));
const mockS3Client = vi.mocked(S3Client);
describe("client.ts", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
const mockConstants = {
S3_ACCESS_KEY: "test-access-key",
S3_SECRET_KEY: "test-secret-key",
S3_REGION: "us-east-1",
S3_ENDPOINT_URL: undefined,
S3_FORCE_PATH_STYLE: false,
};
describe("createS3ClientFromEnv", () => {
test("should create S3 client with valid credentials", async () => {
// Mock constants with valid credentials
vi.doMock("./constants", () => mockConstants);
// Dynamic import to get fresh module with mocked constants
const { createS3ClientFromEnv } = await import("./client");
const client = createS3ClientFromEnv();
expect(mockS3Client).toHaveBeenCalledWith({
credentials: {
accessKeyId: mockConstants.S3_ACCESS_KEY,
secretAccessKey: mockConstants.S3_SECRET_KEY,
},
region: mockConstants.S3_REGION,
forcePathStyle: mockConstants.S3_FORCE_PATH_STYLE,
});
expect(client).toBeDefined();
});
test("should create S3 client with endpoint URL", async () => {
// Mock constants with endpoint URL
vi.doMock("./constants", () => ({
...mockConstants,
S3_ENDPOINT_URL: "https://custom-endpoint.com",
S3_FORCE_PATH_STYLE: true,
}));
const { createS3ClientFromEnv } = await import("./client");
const client = createS3ClientFromEnv();
expect(mockS3Client).toHaveBeenCalledWith({
credentials: {
accessKeyId: mockConstants.S3_ACCESS_KEY,
secretAccessKey: mockConstants.S3_SECRET_KEY,
},
region: mockConstants.S3_REGION,
endpoint: "https://custom-endpoint.com",
forcePathStyle: true,
});
expect(client).toBeDefined();
});
test("should throw error when access key is missing", async () => {
// Mock constants with missing access key
vi.doMock("./constants", () => ({
...mockConstants,
S3_ACCESS_KEY: undefined,
}));
const { createS3ClientFromEnv } = await import("./client");
expect(() => createS3ClientFromEnv()).toThrow("S3 credentials are not set");
});
test("should throw error when secret key is missing", async () => {
// Mock constants with missing secret key
vi.doMock("./constants", () => ({
...mockConstants,
S3_SECRET_KEY: undefined,
}));
const { createS3ClientFromEnv } = await import("./client");
expect(() => createS3ClientFromEnv()).toThrow("S3 credentials are not set");
});
test("should throw error when both credentials are missing", async () => {
// Mock constants with no credentials
vi.doMock("./constants", () => ({
...mockConstants,
S3_ACCESS_KEY: undefined,
S3_SECRET_KEY: undefined,
}));
const { createS3ClientFromEnv } = await import("./client");
expect(() => createS3ClientFromEnv()).toThrow("S3 credentials are not set");
});
test("should throw error when credentials are empty strings", async () => {
// Mock constants with empty string credentials
vi.doMock("./constants", () => ({
...mockConstants,
S3_ACCESS_KEY: "",
S3_SECRET_KEY: "",
}));
const { createS3ClientFromEnv } = await import("./client");
expect(() => createS3ClientFromEnv()).toThrow("S3 credentials are not set");
});
test("should handle mixed empty and undefined credentials", async () => {
// Mock constants with mixed empty and undefined
vi.doMock("./constants", () => ({
...mockConstants,
S3_ACCESS_KEY: "",
S3_SECRET_KEY: undefined,
}));
const { createS3ClientFromEnv } = await import("./client");
expect(() => createS3ClientFromEnv()).toThrow("S3 credentials are not set");
});
test("should handle empty endpoint URL", async () => {
// Mock constants with empty endpoint URL
vi.doMock("./constants", () => ({
...mockConstants,
S3_ENDPOINT_URL: "",
}));
const { createS3ClientFromEnv } = await import("./client");
const client = createS3ClientFromEnv();
expect(mockS3Client).toHaveBeenCalledWith({
credentials: {
accessKeyId: mockConstants.S3_ACCESS_KEY,
secretAccessKey: mockConstants.S3_SECRET_KEY,
},
region: mockConstants.S3_REGION,
forcePathStyle: mockConstants.S3_FORCE_PATH_STYLE,
});
expect(client).toBeDefined();
});
});
describe("createS3Client", () => {
test("should return provided S3 client when passed", async () => {
// Use a fresh import to avoid module cache issues
const { createS3Client } = await import("./client");
const mockClient = new S3Client({});
const result = createS3Client(mockClient);
expect(result).toBe(mockClient);
});
test("should create new client from environment when no client provided", async () => {
// Mock constants for this test
vi.doMock("./constants", () => ({
...mockConstants,
}));
const { createS3Client } = await import("./client");
const result = createS3Client();
expect(mockS3Client).toHaveBeenCalledWith({
credentials: {
accessKeyId: mockConstants.S3_ACCESS_KEY,
secretAccessKey: mockConstants.S3_SECRET_KEY,
},
region: mockConstants.S3_REGION,
forcePathStyle: mockConstants.S3_FORCE_PATH_STYLE,
});
expect(result).toBeDefined();
});
test("should throw error when creating from env fails and no client provided", async () => {
// Mock constants with missing credentials
vi.doMock("./constants", () => ({
...mockConstants,
S3_ACCESS_KEY: undefined,
S3_SECRET_KEY: undefined,
}));
const { createS3Client } = await import("./client");
expect(() => createS3Client()).toThrow("S3 credentials are not set");
});
});
});

View File

@@ -1,27 +1,36 @@
import { S3Client } from "@aws-sdk/client-s3";
import { S3_ACCESS_KEY, S3_ENDPOINT_URL, S3_FORCE_PATH_STYLE, S3_REGION, S3_SECRET_KEY } from "./constants";
const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY;
const S3_SECRET_KEY = process.env.S3_SECRET_KEY;
const S3_REGION = process.env.S3_REGION;
const S3_ENDPOINT_URL = process.env.S3_ENDPOINT_URL;
const S3_FORCE_PATH_STYLE = process.env.S3_FORCE_PATH_STYLE;
/**
* Create an S3 client from environment variables
* @returns An S3 client
* @throws An error if the S3 credentials are not set
*/
export const createS3ClientFromEnv = (): S3Client => {
const credentials =
S3_ACCESS_KEY && S3_SECRET_KEY
? { accessKeyId: S3_ACCESS_KEY, secretAccessKey: S3_SECRET_KEY }
: undefined;
if (!credentials) {
throw new Error("S3 credentials are not set");
}
const s3ClientInstance = new S3Client({
credentials,
region: S3_REGION,
...(S3_ENDPOINT_URL && { endpoint: S3_ENDPOINT_URL }),
forcePathStyle: S3_FORCE_PATH_STYLE === "1",
forcePathStyle: S3_FORCE_PATH_STYLE,
});
return s3ClientInstance;
};
/**
* Create an S3 client from an existing client or from environment variables
* @param s3Client - An existing S3 client
* @returns An S3 client
*/
export const createS3Client = (s3Client?: S3Client): S3Client => {
const client = s3Client ?? createS3ClientFromEnv();
return client;

View File

@@ -0,0 +1,175 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
describe("constants.ts", () => {
const originalEnv = process.env;
beforeEach(() => {
vi.resetModules();
// Reset process.env to a clean state
process.env = { ...originalEnv };
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
});
describe("environment variable exports", () => {
test("should export S3_ACCESS_KEY from environment", async () => {
process.env.S3_ACCESS_KEY = "test-access-key-123";
const { S3_ACCESS_KEY } = await import("./constants");
expect(S3_ACCESS_KEY).toBe("test-access-key-123");
});
test("should export undefined when S3_ACCESS_KEY is not set", async () => {
delete process.env.S3_ACCESS_KEY;
const { S3_ACCESS_KEY } = await import("./constants");
expect(S3_ACCESS_KEY).toBeUndefined();
});
test("should export S3_SECRET_KEY from environment", async () => {
process.env.S3_SECRET_KEY = "test-secret-key-456";
const { S3_SECRET_KEY } = await import("./constants");
expect(S3_SECRET_KEY).toBe("test-secret-key-456");
});
test("should export undefined when S3_SECRET_KEY is not set", async () => {
delete process.env.S3_SECRET_KEY;
const { S3_SECRET_KEY } = await import("./constants");
expect(S3_SECRET_KEY).toBeUndefined();
});
test("should export S3_REGION from environment", async () => {
process.env.S3_REGION = "eu-west-1";
const { S3_REGION } = await import("./constants");
expect(S3_REGION).toBe("eu-west-1");
});
test("should export undefined when S3_REGION is not set", async () => {
delete process.env.S3_REGION;
const { S3_REGION } = await import("./constants");
expect(S3_REGION).toBeUndefined();
});
test("should export S3_ENDPOINT_URL from environment", async () => {
process.env.S3_ENDPOINT_URL = "https://custom-s3-endpoint.com";
const { S3_ENDPOINT_URL } = await import("./constants");
expect(S3_ENDPOINT_URL).toBe("https://custom-s3-endpoint.com");
});
test("should export undefined when S3_ENDPOINT_URL is not set", async () => {
delete process.env.S3_ENDPOINT_URL;
const { S3_ENDPOINT_URL } = await import("./constants");
expect(S3_ENDPOINT_URL).toBeUndefined();
});
test("should export S3_BUCKET_NAME from environment", async () => {
process.env.S3_BUCKET_NAME = "my-storage-bucket";
const { S3_BUCKET_NAME } = await import("./constants");
expect(S3_BUCKET_NAME).toBe("my-storage-bucket");
});
test("should export undefined when S3_BUCKET_NAME is not set", async () => {
delete process.env.S3_BUCKET_NAME;
const { S3_BUCKET_NAME } = await import("./constants");
expect(S3_BUCKET_NAME).toBeUndefined();
});
});
describe("boolean conversion constants", () => {
describe("S3_FORCE_PATH_STYLE", () => {
test("should be true when S3_FORCE_PATH_STYLE is '1'", async () => {
process.env.S3_FORCE_PATH_STYLE = "1";
const { S3_FORCE_PATH_STYLE } = await import("./constants");
expect(S3_FORCE_PATH_STYLE).toBe(true);
});
test("should be false when S3_FORCE_PATH_STYLE is '0'", async () => {
process.env.S3_FORCE_PATH_STYLE = "0";
const { S3_FORCE_PATH_STYLE } = await import("./constants");
expect(S3_FORCE_PATH_STYLE).toBe(false);
});
test("should be false when S3_FORCE_PATH_STYLE is undefined", async () => {
delete process.env.S3_FORCE_PATH_STYLE;
const { S3_FORCE_PATH_STYLE } = await import("./constants");
expect(S3_FORCE_PATH_STYLE).toBe(false);
});
});
describe("IS_FORMBRICKS_CLOUD", () => {
test("should be true when IS_FORMBRICKS_CLOUD is '1'", async () => {
process.env.IS_FORMBRICKS_CLOUD = "1";
const { IS_FORMBRICKS_CLOUD } = await import("./constants");
expect(IS_FORMBRICKS_CLOUD).toBe(true);
});
test("should be false when IS_FORMBRICKS_CLOUD is '0'", async () => {
process.env.IS_FORMBRICKS_CLOUD = "0";
const { IS_FORMBRICKS_CLOUD } = await import("./constants");
expect(IS_FORMBRICKS_CLOUD).toBe(false);
});
test("should be false when IS_FORMBRICKS_CLOUD is undefined", async () => {
delete process.env.IS_FORMBRICKS_CLOUD;
const { IS_FORMBRICKS_CLOUD } = await import("./constants");
expect(IS_FORMBRICKS_CLOUD).toBe(false);
});
});
});
describe("MAX_SIZES constant", () => {
test("should export correct MAX_SIZES object", async () => {
const { MAX_SIZES } = await import("./constants");
expect(MAX_SIZES).toEqual({
standard: 1024 * 1024 * 10, // 10MB
big: 1024 * 1024 * 1024, // 1GB
});
});
test("should have standard size of 10MB", async () => {
const { MAX_SIZES } = await import("./constants");
expect(MAX_SIZES.standard).toBe(10485760); // 10 * 1024 * 1024
});
test("should have big size of 1GB", async () => {
const { MAX_SIZES } = await import("./constants");
expect(MAX_SIZES.big).toBe(1073741824); // 1024 * 1024 * 1024
});
});
});

View File

@@ -1,80 +1 @@
import { DeleteObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { type PresignedPostOptions, createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { createS3Client } from "./client";
import { IS_FORMBRICKS_CLOUD, MAX_SIZES, S3_BUCKET_NAME } from "./constants";
const s3Client = createS3Client();
/**
* Get a signed URL for uploading a file to S3
* @param fileName - The name of the file to upload
* @param contentType - The content type of the file
* @param filePath - The path to the file in S3
* @param isBiggerFileUploadAllowed - Whether to allow uploading bigger files
* @returns A signed URL for the file
*/
export const getSignedUploadUrl = async (
fileName: string,
contentType: string,
filePath: string,
isBiggerFileUploadAllowed = false
): Promise<{
signedUrl: string;
presignedFields: PresignedPostOptions["Fields"];
}> => {
let maxSize = MAX_SIZES.standard;
if (IS_FORMBRICKS_CLOUD) {
maxSize = isBiggerFileUploadAllowed ? MAX_SIZES.big : MAX_SIZES.standard;
} else {
maxSize = Infinity;
}
const postConditions: PresignedPostOptions["Conditions"] = IS_FORMBRICKS_CLOUD
? [["content-length-range", 0, maxSize]]
: undefined;
const { fields, url } = await createPresignedPost(s3Client, {
Expires: 10 * 60, // 10 minutes
Bucket: process.env.S3_BUCKET_NAME ?? "",
Key: `${filePath}/${fileName}`,
Fields: {
"Content-Type": contentType,
"Content-Encoding": "base64",
},
Conditions: postConditions,
});
return {
signedUrl: url,
presignedFields: fields,
};
};
/**
* Get a signed URL for a file in S3
* @param filePath - The path to the file in S3
* @returns A signed URL for the file
*/
export const getSignedDownloadUrl = async (filePath: string): Promise<string> => {
const getObjectCommand = new GetObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: filePath,
});
return await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 60 * 30 });
};
/**
* Delete a file from S3
* @param filePath - The path to the file in S3
*/
export const deleteFile = async (filePath: string): Promise<void> => {
const deleteObjectCommand = new DeleteObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: filePath,
});
await s3Client.send(deleteObjectCommand);
};
export { deleteFile, getSignedDownloadUrl, getSignedUploadUrl } from "./service";

View File

@@ -0,0 +1,323 @@
import { DeleteObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { beforeEach, describe, expect, test, vi } from "vitest";
// Mock AWS SDK modules
vi.mock("@aws-sdk/client-s3", () => ({
DeleteObjectCommand: vi.fn(),
GetObjectCommand: vi.fn(),
}));
vi.mock("@aws-sdk/s3-presigned-post", () => ({
createPresignedPost: vi.fn(),
}));
vi.mock("@aws-sdk/s3-request-presigner", () => ({
getSignedUrl: vi.fn(),
}));
// Mock client module
vi.mock("./client", () => ({
createS3Client: vi.fn(() => ({
send: vi.fn(),
})),
}));
const mockDeleteObjectCommand = vi.mocked(DeleteObjectCommand);
const mockGetObjectCommand = vi.mocked(GetObjectCommand);
const mockCreatePresignedPost = vi.mocked(createPresignedPost);
const mockGetSignedUrl = vi.mocked(getSignedUrl);
describe("service.ts", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
const mockConstants = {
IS_FORMBRICKS_CLOUD: false,
MAX_SIZES: {
standard: 1024 * 1024 * 10, // 10MB
big: 1024 * 1024 * 1024, // 1GB
},
S3_BUCKET_NAME: "test-bucket",
};
describe("getSignedUploadUrl", () => {
test("should create presigned upload URL for non-Formbricks cloud environment", async () => {
// Mock constants for non-cloud environment
vi.doMock("./constants", () => mockConstants);
// Mock createPresignedPost response
const mockResponse = {
fields: { key: "test-field" },
url: "https://example.com",
};
mockCreatePresignedPost.mockResolvedValueOnce(mockResponse);
const { getSignedUploadUrl } = await import("./service");
const result = await getSignedUploadUrl("test-file.jpg", "image/jpeg", "uploads/images", false);
expect(mockCreatePresignedPost).toHaveBeenCalledWith(expect.any(Object), {
Expires: 10 * 60,
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "uploads/images/test-file.jpg",
Fields: {
"Content-Type": "image/jpeg",
"Content-Encoding": "base64",
},
Conditions: undefined, // No conditions for non-cloud
});
expect(result).toEqual({
signedUrl: mockResponse.url,
presignedFields: mockResponse.fields,
});
});
test("should create presigned upload URL for Formbricks cloud with standard size", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
IS_FORMBRICKS_CLOUD: true,
}));
const mockResponse = {
fields: { policy: "test-policy" },
url: "https://example.com",
};
mockCreatePresignedPost.mockResolvedValueOnce(mockResponse);
const { getSignedUploadUrl } = await import("./service");
const result = await getSignedUploadUrl("document.pdf", "application/pdf", "documents", false);
expect(mockCreatePresignedPost).toHaveBeenCalledWith(expect.any(Object), {
Expires: 10 * 60,
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "documents/document.pdf",
Fields: {
"Content-Type": "application/pdf",
"Content-Encoding": "base64",
},
Conditions: [["content-length-range", 0, mockConstants.MAX_SIZES.standard]],
});
expect(result).toEqual({
signedUrl: mockResponse.url,
presignedFields: mockResponse.fields,
});
});
test("should create presigned upload URL for Formbricks cloud with big file size", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
IS_FORMBRICKS_CLOUD: true,
}));
const mockResponse = {
fields: { signature: "test-signature" },
url: "https://example.com",
};
mockCreatePresignedPost.mockResolvedValueOnce(mockResponse);
const { getSignedUploadUrl } = await import("./service");
const result = await getSignedUploadUrl("large-video.mp4", "video/mp4", "videos", true);
expect(mockCreatePresignedPost).toHaveBeenCalledWith(expect.any(Object), {
Expires: 10 * 60,
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "videos/large-video.mp4",
Fields: {
"Content-Type": "video/mp4",
"Content-Encoding": "base64",
},
Conditions: [["content-length-range", 0, mockConstants.MAX_SIZES.big]],
});
expect(result).toEqual({
signedUrl: mockResponse.url,
presignedFields: mockResponse.fields,
});
});
test("should handle undefined bucket name", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
S3_BUCKET_NAME: undefined,
}));
const mockResponse = {
fields: {},
url: "https://example.com",
};
mockCreatePresignedPost.mockResolvedValueOnce(mockResponse);
const { getSignedUploadUrl } = await import("./service");
await getSignedUploadUrl("test.txt", "text/plain", "text", false);
expect(mockCreatePresignedPost).toHaveBeenCalledWith(expect.any(Object), {
Expires: 10 * 60,
Bucket: "", // Should default to empty string
Key: "text/test.txt",
Fields: {
"Content-Type": "text/plain",
"Content-Encoding": "base64",
},
Conditions: undefined,
});
});
test("should use default value for isBiggerFileUploadAllowed parameter", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
IS_FORMBRICKS_CLOUD: true,
}));
const mockResponse = {
fields: {},
url: "https://test.com",
};
mockCreatePresignedPost.mockResolvedValueOnce(mockResponse);
const { getSignedUploadUrl } = await import("./service");
// Call without isBiggerFileUploadAllowed parameter (should default to false)
await getSignedUploadUrl("test.jpg", "image/jpeg", "images");
expect(mockCreatePresignedPost).toHaveBeenCalledWith(expect.any(Object), {
Expires: 10 * 60,
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "images/test.jpg",
Fields: {
"Content-Type": "image/jpeg",
"Content-Encoding": "base64",
},
Conditions: [["content-length-range", 0, mockConstants.MAX_SIZES.standard]],
});
});
});
describe("getSignedDownloadUrl", () => {
test("should create signed download URL", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
}));
const mockSignedUrl = "https://example.com/important-file.pdf?signature=abc123";
mockGetSignedUrl.mockResolvedValueOnce(mockSignedUrl);
const { getSignedDownloadUrl } = await import("./service");
const result = await getSignedDownloadUrl("documents/important-file.pdf");
expect(mockGetObjectCommand).toHaveBeenCalledWith({
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "documents/important-file.pdf",
});
expect(mockGetSignedUrl).toHaveBeenCalledWith(
expect.any(Object), // s3Client
expect.any(Object), // GetObjectCommand instance
{ expiresIn: 60 * 30 } // 30 minutes
);
expect(result).toBe(mockSignedUrl);
});
test("should handle different file keys", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
}));
mockGetSignedUrl.mockResolvedValueOnce("https://example.com/nested/file.jpg");
const { getSignedDownloadUrl } = await import("./service");
await getSignedDownloadUrl("path/to/nested/file.jpg");
expect(mockGetObjectCommand).toHaveBeenCalledWith({
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "path/to/nested/file.jpg",
});
});
});
describe("deleteFile", () => {
test("should delete file from S3", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
}));
const mockS3Client = {
send: vi.fn().mockResolvedValueOnce({}),
};
vi.doMock("./client", () => ({
createS3Client: vi.fn(() => mockS3Client),
}));
const { deleteFile } = await import("./service");
await deleteFile("files/to-delete.txt");
expect(mockDeleteObjectCommand).toHaveBeenCalledWith({
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "files/to-delete.txt",
});
expect(mockS3Client.send).toHaveBeenCalledWith(expect.any(Object));
});
test("should handle different file keys for deletion", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
}));
const mockS3Client = {
send: vi.fn().mockResolvedValueOnce({}),
};
vi.doMock("./client", () => ({
createS3Client: vi.fn(() => mockS3Client),
}));
const { deleteFile } = await import("./service");
await deleteFile("deep/nested/path/file.zip");
expect(mockDeleteObjectCommand).toHaveBeenCalledWith({
Bucket: mockConstants.S3_BUCKET_NAME,
Key: "deep/nested/path/file.zip",
});
});
test("should not return anything", async () => {
vi.doMock("./constants", () => ({
...mockConstants,
}));
const mockS3Client = {
send: vi.fn().mockResolvedValueOnce({}),
};
vi.doMock("./client", () => ({
createS3Client: vi.fn(() => mockS3Client),
}));
const { deleteFile } = await import("./service");
await deleteFile("test-file.txt");
expect(mockS3Client.send).toHaveBeenCalledWith(expect.any(Object));
});
});
});

View File

@@ -0,0 +1,80 @@
import { DeleteObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { type PresignedPostOptions, createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { createS3Client } from "./client";
import { IS_FORMBRICKS_CLOUD, MAX_SIZES, S3_BUCKET_NAME } from "./constants";
const s3Client = createS3Client();
/**
* Get a signed URL for uploading a file to S3
* @param fileName - The name of the file to upload
* @param contentType - The content type of the file
* @param filePath - The path to the file in S3
* @param isBiggerFileUploadAllowed - Whether to allow uploading bigger files
* @returns A signed URL for the file
*/
export const getSignedUploadUrl = async (
fileName: string,
contentType: string,
filePath: string,
isBiggerFileUploadAllowed = false
): Promise<{
signedUrl: string;
presignedFields: PresignedPostOptions["Fields"];
}> => {
let maxSize = MAX_SIZES.standard;
if (IS_FORMBRICKS_CLOUD) {
maxSize = isBiggerFileUploadAllowed ? MAX_SIZES.big : MAX_SIZES.standard;
} else {
maxSize = Infinity;
}
const postConditions: PresignedPostOptions["Conditions"] = IS_FORMBRICKS_CLOUD
? [["content-length-range", 0, maxSize]]
: undefined;
const { fields, url } = await createPresignedPost(s3Client, {
Expires: 10 * 60, // 10 minutes
Bucket: S3_BUCKET_NAME ?? "",
Key: `${filePath}/${fileName}`,
Fields: {
"Content-Type": contentType,
"Content-Encoding": "base64",
},
Conditions: postConditions,
});
return {
signedUrl: url,
presignedFields: fields,
};
};
/**
* Get a signed URL for a file in S3
* @param fileKey - The key of the file in S3
* @returns A signed URL for the file
*/
export const getSignedDownloadUrl = async (fileKey: string): Promise<string> => {
const getObjectCommand = new GetObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: fileKey,
});
return await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 60 * 30 });
};
/**
* Delete a file from S3
* @param fileKey - The key of the file in S3
*/
export const deleteFile = async (fileKey: string): Promise<void> => {
const deleteObjectCommand = new DeleteObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: fileKey,
});
await s3Client.send(deleteObjectCommand);
};

View File

@@ -11,9 +11,9 @@ export default defineConfig({
fileName: "index",
formats: ["es", "cjs"],
},
// rollupOptions: {
// external: ["@aws-sdk/client-s3", "@aws-sdk/s3-presigned-post", "@aws-sdk/s3-request-presigner"],
// },
rollupOptions: {
external: ["@aws-sdk/client-s3", "@aws-sdk/s3-presigned-post", "@aws-sdk/s3-request-presigner"],
},
},
test: {
environment: "node",

View File

@@ -6,12 +6,12 @@ sonar.sources=apps/web,packages/surveys,packages/js-core
sonar.exclusions=**/node_modules/**,**/.next/**,**/dist/**,**/build/**,**/*.test.*,**/*.spec.*,**/__mocks__/**
# Tests
sonar.tests=apps/web,packages/surveys,packages/js-core
sonar.tests=apps/web,packages/surveys,packages/js-core,packages/storage
sonar.test.inclusions=**/*.test.*,**/*.spec.*
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info,packages/surveys/coverage/lcov.info,packages/js-core/coverage/lcov.info
sonar.javascript.lcov.reportPaths=apps/web/coverage/lcov.info,packages/surveys/coverage/lcov.info,packages/js-core/coverage/lcov.info,packages/storage/coverage/lcov.info
# TypeScript configuration
sonar.typescript.tsconfigPath=apps/web/tsconfig.json,packages/surveys/tsconfig.json,packages/js-core/tsconfig.json
sonar.typescript.tsconfigPath=apps/web/tsconfig.json,packages/surveys/tsconfig.json,packages/js-core/tsconfig.json,packages/storage/tsconfig.json
# SCM
sonar.scm.provider=git