mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-14 18:54:40 -05:00
fixes
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "找不到環境權限",
|
||||
|
||||
@@ -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>",
|
||||
|
||||
205
packages/storage/src/client.test.ts
Normal file
205
packages/storage/src/client.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
175
packages/storage/src/constants.test.ts
Normal file
175
packages/storage/src/constants.test.ts
Normal 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
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
323
packages/storage/src/service.test.ts
Normal file
323
packages/storage/src/service.test.ts
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
80
packages/storage/src/service.ts
Normal file
80
packages/storage/src/service.ts
Normal 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);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user