Files
formbricks/apps/web/modules/storage/service.test.ts
Anshuman Pandey ff10ca7d6a fix: allows local ip images (#7189)
Co-authored-by: pandeymangg <pandeyman@Anshumans-MacBook-Air.local>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Matti Nannt <matti@formbricks.com>
2026-02-10 17:29:27 +01:00

487 lines
15 KiB
TypeScript

import { randomUUID } from "crypto";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { StorageErrorCode } from "@formbricks/storage";
import { TAccessType } from "@formbricks/types/storage";
import {
deleteFile,
deleteFilesByEnvironmentId,
getFileStreamForDownload,
getSignedUrlForUpload,
} from "./service";
// Mock external dependencies
vi.mock("crypto", () => ({
randomUUID: vi.fn(),
}));
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
},
}));
vi.mock("@formbricks/storage", () => ({
StorageErrorCode: {
Unknown: "unknown",
S3ClientError: "s3_client_error",
S3CredentialsError: "s3_credentials_error",
FileNotFoundError: "file_not_found_error",
InvalidInput: "invalid_input",
},
deleteFile: vi.fn(),
deleteFilesByPrefix: vi.fn(),
getFileStream: vi.fn(),
getSignedDownloadUrl: vi.fn(),
getSignedUploadUrl: vi.fn(),
}));
// Import mocked dependencies
const { logger } = await import("@formbricks/logger");
const storageModule = await import("@formbricks/storage");
const {
deleteFile: deleteFileFromS3,
deleteFilesByPrefix,
getSignedUploadUrl,
getFileStream,
} = storageModule;
type MockedSignedUploadReturn = Awaited<ReturnType<typeof getSignedUploadUrl>>;
type MockedFileStreamReturn = Awaited<ReturnType<typeof getFileStream>>;
type MockedDeleteFileReturn = Awaited<ReturnType<typeof deleteFile>>;
type MockedDeleteFilesByPrefixReturn = Awaited<ReturnType<typeof deleteFilesByPrefix>>;
const mockUUID = "test-uuid-123-456-789-10";
describe("storage service", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(randomUUID).mockReturnValue(mockUUID);
});
describe("getSignedUrlForUpload", () => {
test("should generate signed URL for upload with unique filename", async () => {
const mockSignedUrlResponse = {
ok: true,
data: {
signedUrl: "https://s3.example.com/upload",
presignedFields: { key: "value" },
},
} as MockedSignedUploadReturn;
vi.mocked(getSignedUploadUrl).mockResolvedValue(mockSignedUrlResponse);
const result = await getSignedUrlForUpload(
"test-image.jpg",
"env-123",
"image/jpeg",
"public" as TAccessType
);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toEqual({
signedUrl: "https://s3.example.com/upload",
presignedFields: { key: "value" },
fileUrl: `/storage/env-123/public/test-image--fid--${mockUUID}.jpg`,
});
}
expect(getSignedUploadUrl).toHaveBeenCalledWith(
`test-image--fid--${mockUUID}.jpg`,
"image/jpeg",
"env-123/public",
1024 * 1024 * 10 // 10MB default
);
});
test("should return relative URL for private files", async () => {
const mockSignedUrlResponse = {
ok: true,
data: {
signedUrl: "https://s3.example.com/upload",
presignedFields: { key: "value" },
},
} as MockedSignedUploadReturn;
vi.mocked(getSignedUploadUrl).mockResolvedValue(mockSignedUrlResponse);
const result = await getSignedUrlForUpload(
"test-doc.pdf",
"env-123",
"application/pdf",
"private" as TAccessType
);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data.fileUrl).toBe(`/storage/env-123/private/test-doc--fid--${mockUUID}.pdf`);
}
});
test("should properly sanitize filenames with special characters like # in URL", async () => {
const mockSignedUrlResponse = {
ok: true,
data: {
signedUrl: "https://s3.example.com/upload",
presignedFields: { key: "value" },
},
} as MockedSignedUploadReturn;
vi.mocked(getSignedUploadUrl).mockResolvedValue(mockSignedUrlResponse);
const result = await getSignedUrlForUpload(
"test#file.txt",
"env-123",
"text/plain",
"public" as TAccessType
);
expect(result.ok).toBe(true);
if (result.ok) {
// The filename should be URL-encoded to prevent # from being treated as a URL fragment
expect(result.data.fileUrl).toBe(`/storage/env-123/public/testfile--fid--${mockUUID}.txt`);
}
expect(getSignedUploadUrl).toHaveBeenCalledWith(
`testfile--fid--${mockUUID}.txt`,
"text/plain",
"env-123/public",
1024 * 1024 * 10 // 10MB default
);
});
test("should handle files with multiple dots in filename", async () => {
const mockSignedUrlResponse = {
ok: true,
data: {
signedUrl: "https://s3.example.com/upload",
presignedFields: { key: "value" },
},
} as MockedSignedUploadReturn;
vi.mocked(getSignedUploadUrl).mockResolvedValue(mockSignedUrlResponse);
const result = await getSignedUrlForUpload(
"my.backup.file.pdf",
"env-123",
"application/pdf",
"public" as TAccessType
);
expect(result.ok).toBe(true);
expect(getSignedUploadUrl).toHaveBeenCalledWith(
`my.backup.file--fid--${mockUUID}.pdf`,
"application/pdf",
"env-123/public",
1024 * 1024 * 10
);
});
test("should use custom maxFileUploadSize when provided", async () => {
const mockSignedUrlResponse = {
ok: true,
data: {
signedUrl: "https://s3.example.com/upload",
presignedFields: { key: "value" },
},
} as MockedSignedUploadReturn;
vi.mocked(getSignedUploadUrl).mockResolvedValue(mockSignedUrlResponse);
await getSignedUrlForUpload(
"large-file.pdf",
"env-123",
"application/pdf",
"public" as TAccessType,
1024 * 1024 * 50 // 50MB
);
expect(getSignedUploadUrl).toHaveBeenCalledWith(
`large-file--fid--${mockUUID}.pdf`,
"application/pdf",
"env-123/public",
1024 * 1024 * 50
);
});
test("should return error when getSignedUploadUrl fails", async () => {
const mockErrorResponse = {
ok: false,
error: {
code: StorageErrorCode.S3ClientError,
},
} as MockedSignedUploadReturn;
vi.mocked(getSignedUploadUrl).mockResolvedValue(mockErrorResponse);
const result = await getSignedUrlForUpload(
"test-file.pdf",
"env-123",
"application/pdf",
"public" as TAccessType
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(StorageErrorCode.S3ClientError);
}
});
test("should handle unexpected errors and return unknown error", async () => {
vi.mocked(getSignedUploadUrl).mockRejectedValue(new Error("Unexpected error"));
const result = await getSignedUrlForUpload(
"test-file.pdf",
"env-123",
"application/pdf",
"public" as TAccessType
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(StorageErrorCode.Unknown);
}
expect(logger.error).toHaveBeenCalledWith(
{ error: expect.any(Error) },
"Error getting signed url for upload"
);
});
test("should return InvalidInput when sanitized filename is empty or invalid", async () => {
const mockErrorResponse = {
ok: false,
error: { code: StorageErrorCode.InvalidInput },
} as MockedSignedUploadReturn;
vi.mocked(getSignedUploadUrl).mockResolvedValue(mockErrorResponse);
const result = await getSignedUrlForUpload("----.png", "env-123", "image/png", "public" as TAccessType);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(StorageErrorCode.InvalidInput);
}
});
});
describe("deleteFile", () => {
test("should call deleteFileFromS3 with correct file key", async () => {
const mockSuccessResult = {
ok: true,
data: undefined,
} as MockedDeleteFileReturn;
vi.mocked(deleteFileFromS3).mockResolvedValue(mockSuccessResult);
const result = await deleteFile("env-123", "public" as TAccessType, "test-file.jpg");
expect(result).toEqual(mockSuccessResult);
expect(deleteFileFromS3).toHaveBeenCalledWith("env-123/public/test-file.jpg");
});
test("should handle private access type", async () => {
const mockSuccessResult = {
ok: true,
data: undefined,
} as MockedDeleteFileReturn;
vi.mocked(deleteFileFromS3).mockResolvedValue(mockSuccessResult);
const result = await deleteFile("env-456", "private" as TAccessType, "private-doc.pdf");
expect(result).toEqual(mockSuccessResult);
expect(deleteFileFromS3).toHaveBeenCalledWith("env-456/private/private-doc.pdf");
});
test("should handle when deleteFileFromS3 returns error", async () => {
const mockErrorResult = {
ok: false,
error: {
code: StorageErrorCode.Unknown,
},
} as MockedDeleteFileReturn;
vi.mocked(deleteFileFromS3).mockResolvedValue(mockErrorResult);
const result = await deleteFile("env-123", "public" as TAccessType, "test-file.jpg");
expect(result).toEqual(mockErrorResult);
expect(deleteFileFromS3).toHaveBeenCalledWith("env-123/public/test-file.jpg");
});
});
describe("deleteFilesByEnvironmentId", () => {
test("should call deleteFilesByPrefix with environment ID", async () => {
const mockSuccessResult = {
ok: true,
data: undefined,
} as MockedDeleteFilesByPrefixReturn;
vi.mocked(deleteFilesByPrefix).mockResolvedValue(mockSuccessResult);
const result = await deleteFilesByEnvironmentId("env-123");
expect(result).toEqual(mockSuccessResult);
expect(deleteFilesByPrefix).toHaveBeenCalledWith("env-123");
});
test("should handle when deleteFilesByPrefix returns error", async () => {
const mockErrorResult = {
ok: false,
error: {
code: StorageErrorCode.Unknown,
},
} as MockedDeleteFilesByPrefixReturn;
vi.mocked(deleteFilesByPrefix).mockResolvedValue(mockErrorResult);
const result = await deleteFilesByEnvironmentId("env-123");
expect(result).toEqual(mockErrorResult);
expect(deleteFilesByPrefix).toHaveBeenCalledWith("env-123");
});
});
describe("getFileStreamForDownload", () => {
test("should return file stream for public file", async () => {
const mockStream = new ReadableStream();
const mockStreamResult = {
ok: true,
data: {
body: mockStream,
contentType: "image/jpeg",
contentLength: 12345,
},
} as MockedFileStreamReturn;
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
const result = await getFileStreamForDownload("test-image.jpg", "env-123", "public" as TAccessType);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data.body).toBe(mockStream);
expect(result.data.contentType).toBe("image/jpeg");
expect(result.data.contentLength).toBe(12345);
}
expect(getFileStream).toHaveBeenCalledWith("env-123/public/test-image.jpg");
});
test("should return file stream for private file", async () => {
const mockStream = new ReadableStream();
const mockStreamResult = {
ok: true,
data: {
body: mockStream,
contentType: "application/pdf",
contentLength: 54321,
},
} as MockedFileStreamReturn;
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
const result = await getFileStreamForDownload("document.pdf", "env-456", "private" as TAccessType);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data.contentType).toBe("application/pdf");
}
expect(getFileStream).toHaveBeenCalledWith("env-456/private/document.pdf");
});
test("should decode URL-encoded filename", async () => {
const mockStream = new ReadableStream();
const mockStreamResult = {
ok: true,
data: {
body: mockStream,
contentType: "image/png",
contentLength: 1000,
},
} as MockedFileStreamReturn;
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
// URL-encoded filename with spaces: "my file.png" -> "my%20file.png"
const result = await getFileStreamForDownload("my%20file.png", "env-123", "public" as TAccessType);
expect(result.ok).toBe(true);
// Should decode %20 to space before passing to getFileStream
expect(getFileStream).toHaveBeenCalledWith("env-123/public/my file.png");
});
test("should return error when getFileStream fails with FileNotFoundError", async () => {
const mockErrorResult = {
ok: false,
error: {
code: StorageErrorCode.FileNotFoundError,
},
} as MockedFileStreamReturn;
vi.mocked(getFileStream).mockResolvedValue(mockErrorResult);
const result = await getFileStreamForDownload("missing-file.jpg", "env-123", "public" as TAccessType);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(StorageErrorCode.FileNotFoundError);
}
});
test("should return error when getFileStream fails with S3ClientError", async () => {
const mockErrorResult = {
ok: false,
error: {
code: StorageErrorCode.S3ClientError,
},
} as MockedFileStreamReturn;
vi.mocked(getFileStream).mockResolvedValue(mockErrorResult);
const result = await getFileStreamForDownload("some-file.jpg", "env-123", "public" as TAccessType);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(StorageErrorCode.S3ClientError);
}
});
test("should handle unexpected errors and return unknown error", async () => {
vi.mocked(getFileStream).mockRejectedValue(new Error("Unexpected S3 error"));
const result = await getFileStreamForDownload("test-file.jpg", "env-123", "public" as TAccessType);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe(StorageErrorCode.Unknown);
}
expect(logger.error).toHaveBeenCalledWith(
{ error: expect.any(Error) },
"Error getting file stream for download"
);
});
test("should handle filename with fid pattern", async () => {
const mockStream = new ReadableStream();
const mockStreamResult = {
ok: true,
data: {
body: mockStream,
contentType: "image/jpeg",
contentLength: 5000,
},
} as MockedFileStreamReturn;
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
const result = await getFileStreamForDownload(
"photo--fid--abc123-def456.jpg",
"env-123",
"public" as TAccessType
);
expect(result.ok).toBe(true);
expect(getFileStream).toHaveBeenCalledWith("env-123/public/photo--fid--abc123-def456.jpg");
});
});
});