add s3Service rest api
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
import {S3Service} from "../services/s3Service";
|
||||||
|
import multer from "multer"
|
||||||
|
import express from "express";
|
||||||
|
import {asyncHandler} from "./middleware/asyncHandler";
|
||||||
|
import {badRequest, created, forbidden, notFound, ok} from "./utils/responses";
|
||||||
|
import {vi} from "vitest";
|
||||||
|
|
||||||
|
vi.mock("../../src/services/db/UserService", () => ({
|
||||||
|
UserService: {
|
||||||
|
create: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export class RestStorage {
|
||||||
|
|
||||||
|
constructor(private readonly s3Service: S3Service) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public createRouter() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: multer.memoryStorage(),
|
||||||
|
limits: {fileSize: 10 * 1024 * 1024},
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/upload",
|
||||||
|
upload.single('image'),
|
||||||
|
asyncHandler(async (req, res) => {
|
||||||
|
if (!req.file) {
|
||||||
|
return badRequest(res, "No file provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.payload.uuid;
|
||||||
|
const objectKey = await this.s3Service.uploadFile(req.file, userId);
|
||||||
|
|
||||||
|
return created(res, {message: "File uploaded successfully", objectKey})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get("/files", asyncHandler(async (req, res) => {
|
||||||
|
const userId = req.payload.uuid;
|
||||||
|
const files = await this.s3Service.listFilesForUser(userId);
|
||||||
|
|
||||||
|
return ok(res, files);
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get(/\/files\/(.*)\/url$/, asyncHandler(async (req, res) => {
|
||||||
|
const userId = req.payload.uuid;
|
||||||
|
const objectKey =req.params[0];
|
||||||
|
|
||||||
|
console.log(userId);
|
||||||
|
console.log(objectKey)
|
||||||
|
|
||||||
|
if (!objectKey || !objectKey.startsWith(`user-${userId}`)) {
|
||||||
|
return forbidden(res);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const expiresInSeconds = 60;
|
||||||
|
const downloadUrl = await this.s3Service.getSignedDownloadUrl(objectKey, expiresInSeconds);
|
||||||
|
|
||||||
|
return ok(res, {url: downloadUrl});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name === "NoSuchKey") {
|
||||||
|
return notFound(res, "File not found.");
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.delete(/\/files\/(.*)/, asyncHandler(async (req, res) => { // <-- ÄNDERUNG HIER
|
||||||
|
const userId = req.payload.uuid;
|
||||||
|
const objectKey =req.params[0];
|
||||||
|
|
||||||
|
console.log(objectKey)
|
||||||
|
if (!objectKey.startsWith(`user-${userId}/`)) {
|
||||||
|
return forbidden(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.s3Service.deleteFile(objectKey);
|
||||||
|
|
||||||
|
return ok(res, "File deleted successfully");
|
||||||
|
}));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -21,6 +21,7 @@ import {disconnectFromDatabase} from "./services/db/database.service";
|
|||||||
import {SpotifyTokenService} from "./services/spotifyTokenService";
|
import {SpotifyTokenService} from "./services/spotifyTokenService";
|
||||||
import {WeatherPollingService} from "./services/weatherPollingService";
|
import {WeatherPollingService} from "./services/weatherPollingService";
|
||||||
import {S3Service} from "./services/s3Service";
|
import {S3Service} from "./services/s3Service";
|
||||||
|
import {RestStorage} from "./rest/restStorage";
|
||||||
|
|
||||||
interface ServerDependencies {
|
interface ServerDependencies {
|
||||||
userService: UserService;
|
userService: UserService;
|
||||||
@@ -65,7 +66,7 @@ export class Server {
|
|||||||
watchUserChanges();
|
watchUserChanges();
|
||||||
|
|
||||||
this._setupMiddleware();
|
this._setupMiddleware();
|
||||||
this._setupRoutes(userService, spotifyTokenService, jwtAuthenticator);
|
this._setupRoutes(userService, spotifyTokenService, jwtAuthenticator, s3Service);
|
||||||
this._setupErrorHandling();
|
this._setupErrorHandling();
|
||||||
|
|
||||||
this.httpServer = this.app.listen(this.config.port, () => {
|
this.httpServer = this.app.listen(this.config.port, () => {
|
||||||
@@ -100,13 +101,14 @@ export class Server {
|
|||||||
this.app.use(express.json({limit: "2mb"}));
|
this.app.use(express.json({limit: "2mb"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setupRoutes(userService: UserService, spotifyTokenService: SpotifyTokenService, jwtAuthenticator: JwtAuthenticator): void {
|
private _setupRoutes(userService: UserService, spotifyTokenService: SpotifyTokenService, jwtAuthenticator: JwtAuthenticator, s3Service: S3Service): void {
|
||||||
const _authenticateJwt = authenticateJwt(jwtAuthenticator);
|
const _authenticateJwt = authenticateJwt(jwtAuthenticator);
|
||||||
|
|
||||||
const restAuth = new RestAuth(userService, jwtAuthenticator);
|
const restAuth = new RestAuth(userService, jwtAuthenticator);
|
||||||
const restUser = new RestUser(userService);
|
const restUser = new RestUser(userService);
|
||||||
const spotifyTokenGenerator = new SpotifyTokenGenerator(spotifyTokenService);
|
const spotifyTokenGenerator = new SpotifyTokenGenerator(spotifyTokenService);
|
||||||
const jwtTokenExtractor = new JwtTokenPropertiesExtractor();
|
const jwtTokenExtractor = new JwtTokenPropertiesExtractor();
|
||||||
|
const storage = new RestStorage(s3Service);
|
||||||
|
|
||||||
this.app.get("/api/healthz", (_req, res) => res.status(200).send({status: "ok"}));
|
this.app.get("/api/healthz", (_req, res) => res.status(200).send({status: "ok"}));
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ export class Server {
|
|||||||
this.app.use("/api/spotify", _authenticateJwt, spotifyLimiter, spotifyTokenGenerator.createRouter());
|
this.app.use("/api/spotify", _authenticateJwt, spotifyLimiter, spotifyTokenGenerator.createRouter());
|
||||||
this.app.use("/api/user", _authenticateJwt, restUser.createRouter());
|
this.app.use("/api/user", _authenticateJwt, restUser.createRouter());
|
||||||
this.app.use("/api/jwt", _authenticateJwt, jwtTokenExtractor.createRouter());
|
this.app.use("/api/jwt", _authenticateJwt, jwtTokenExtractor.createRouter());
|
||||||
|
this.app.use("/api/storage", _authenticateJwt, storage.createRouter());
|
||||||
|
|
||||||
this.app.use("/api/websocket", _authenticateJwt, (req, res, next) => {
|
this.app.use("/api/websocket", _authenticateJwt, (req, res, next) => {
|
||||||
if (this.webSocketServer) {
|
if (this.webSocketServer) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
S3Client,
|
S3Client,
|
||||||
CreateBucketCommand,
|
CreateBucketCommand,
|
||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
GetObjectCommand
|
GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand
|
||||||
} from "@aws-sdk/client-s3";
|
} from "@aws-sdk/client-s3";
|
||||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
@@ -74,6 +74,30 @@ export class S3Service {
|
|||||||
return objectKey;
|
return objectKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listFilesForUser(userId: string): Promise<{ key: string, lastModified: Date }[]> {
|
||||||
|
const command = new ListObjectsV2Command({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Prefix: `user-${userId}/`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.client.send(command);
|
||||||
|
|
||||||
|
return response.Contents?.map(item => ({
|
||||||
|
key: item.Key!,
|
||||||
|
lastModified: item.LastModified!,
|
||||||
|
})) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(objectKey: string): Promise<void> {
|
||||||
|
const command = new DeleteObjectCommand({
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Key: objectKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.client.send(command);
|
||||||
|
console.log(`File deleted: ${objectKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
async getSignedDownloadUrl(objectKey: string, expiresIn: number = 60): Promise<string> {
|
async getSignedDownloadUrl(objectKey: string, expiresIn: number = 60): Promise<string> {
|
||||||
const command = new GetObjectCommand({
|
const command = new GetObjectCommand({
|
||||||
Bucket: this.bucketName,
|
Bucket: this.bucketName,
|
||||||
|
|||||||
@@ -124,4 +124,12 @@ export const createMockSpotifyApiService = () => ({
|
|||||||
export const createMockSpotifyPollingService = () => ({
|
export const createMockSpotifyPollingService = () => ({
|
||||||
startPollingForUser: vi.fn(),
|
startPollingForUser: vi.fn(),
|
||||||
stopPollingForUser: vi.fn()
|
stopPollingForUser: vi.fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createMockS3Service = () => ({
|
||||||
|
ensureBucketExists: vi.fn(),
|
||||||
|
uploadFile: vi.fn(),
|
||||||
|
listFilesForUser: vi.fn(),
|
||||||
|
deleteFile: vi.fn(),
|
||||||
|
getSignedDownloadUrl: vi.fn(),
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest";
|
||||||
|
import request from "supertest";
|
||||||
|
import express from "express";
|
||||||
|
import {RestStorage} from "../../src/rest/restStorage";
|
||||||
|
import {S3Service} from "../../src/services/s3Service";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import {createMockS3Service, createTestApp} from "../helpers/testSetup";
|
||||||
|
|
||||||
|
vi.mock("../../src/services/s3Service");
|
||||||
|
|
||||||
|
describe("RestStorage", () => {
|
||||||
|
let app: express.Application;
|
||||||
|
let mockS3Service: any;
|
||||||
|
|
||||||
|
const requestingUserUUID = "user-id-123";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockS3Service = createMockS3Service();
|
||||||
|
|
||||||
|
const restStorage = new RestStorage(mockS3Service as unknown as S3Service);
|
||||||
|
|
||||||
|
app = createTestApp(restStorage.createRouter(), "/storage", {
|
||||||
|
uuid: requestingUserUUID,
|
||||||
|
name: "name",
|
||||||
|
id: "1234"
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /upload", () => {
|
||||||
|
it("should upload a file and return a 201 Created response", async () => {
|
||||||
|
const objectKey = `user-${requestingUserUUID}/generated-uuid.jpg`;
|
||||||
|
mockS3Service.uploadFile.mockResolvedValue(objectKey);
|
||||||
|
|
||||||
|
const response = await request(app) // Verwende die 'app' aus dem beforeEach
|
||||||
|
.post("/storage/upload")
|
||||||
|
.attach('image', Buffer.from("fake image data"), "test.jpg")
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
expect(response.body.data).toEqual({
|
||||||
|
message: "File uploaded successfully",
|
||||||
|
objectKey: objectKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockS3Service.uploadFile).toHaveBeenCalledOnce();
|
||||||
|
expect(mockS3Service.uploadFile).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
originalname: 'test.jpg'
|
||||||
|
}),
|
||||||
|
requestingUserUUID
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a 400 Bad Request if no file is provided", async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.post("/storage/upload")
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(response.body.data.message).toBe("No file provided.");
|
||||||
|
expect(mockS3Service.uploadFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /files", () => {
|
||||||
|
it("should return a list of files for the current user", async () => {
|
||||||
|
const mockFiles = [
|
||||||
|
{key: `user-${requestingUserUUID}/file1.txt`, lastModified: new Date()},
|
||||||
|
{key: `user-${requestingUserUUID}/image.png`, lastModified: new Date()},
|
||||||
|
];
|
||||||
|
mockS3Service.listFilesForUser.mockResolvedValue(mockFiles);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get("/storage/files")
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const responseData = JSON.parse(JSON.stringify(mockFiles));
|
||||||
|
expect(response.body.data).toEqual(responseData);
|
||||||
|
expect(mockS3Service.listFilesForUser).toHaveBeenCalledOnce();
|
||||||
|
expect(mockS3Service.listFilesForUser).toHaveBeenCalledWith(requestingUserUUID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("GET /files/:key/url", () => { // Beschreibung an die Logik angepasst
|
||||||
|
it("should return a signed URL for a file owned by the user", async () => {
|
||||||
|
const objectKey = `user-${requestingUserUUID}/my-photo.jpg`;
|
||||||
|
const signedUrl = "http://s3.com/signed-url";
|
||||||
|
mockS3Service.getSignedDownloadUrl.mockResolvedValue(signedUrl);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/storage/files/${encodeURIComponent(objectKey)}/url`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toEqual({url: signedUrl});
|
||||||
|
expect(mockS3Service.getSignedDownloadUrl).toHaveBeenCalledOnce();
|
||||||
|
expect(mockS3Service.getSignedDownloadUrl).toHaveBeenCalledWith(objectKey, 60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 403 Forbidden if the user tries to access another user's file", async () => {
|
||||||
|
const objectKey = "user-another-user/secret.txt";
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.get(`/storage/files/${encodeURIComponent(objectKey)}/url`)
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(mockS3Service.getSignedDownloadUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 404 Not Found if the file does not exist", async () => {
|
||||||
|
const objectKey = `user-${requestingUserUUID}/non-existent.jpg`;
|
||||||
|
mockS3Service.getSignedDownloadUrl.mockRejectedValue({name: "NoSuchKey"});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/storage/files/${encodeURIComponent(objectKey)}/url`)
|
||||||
|
.expect(404);
|
||||||
|
|
||||||
|
expect(response.body.data.message).toBe("File not found.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DELETE /files/:key", () => { // Beschreibung an die Logik angepasst
|
||||||
|
it("should delete a file owned by the user", async () => {
|
||||||
|
const objectKey = `user-${requestingUserUUID}/file-to-delete.pdf`;
|
||||||
|
mockS3Service.deleteFile.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.delete(`/storage/files/${encodeURIComponent(objectKey)}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toBe("File deleted successfully");
|
||||||
|
expect(mockS3Service.deleteFile).toHaveBeenCalledOnce();
|
||||||
|
expect(mockS3Service.deleteFile).toHaveBeenCalledWith(objectKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 403 Forbidden if the user tries to delete another user's file", async () => {
|
||||||
|
const objectKey = "user-another-user/data.csv";
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.delete(`/storage/files/${encodeURIComponent(objectKey)}`)
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(mockS3Service.deleteFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,8 +11,14 @@ vi.mock('@aws-sdk/client-s3', async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { S3Service, S3ClientConfig } from '../../src/services/s3Service';
|
import { S3Service, S3ClientConfig } from '../../src/services/s3Service';
|
||||||
import { S3Client, CreateBucketCommand, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
import {
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
S3Client,
|
||||||
|
CreateBucketCommand,
|
||||||
|
PutObjectCommand,
|
||||||
|
GetObjectCommand,
|
||||||
|
ListObjectsV2Command,
|
||||||
|
DeleteObjectCommand
|
||||||
|
} from '@aws-sdk/client-s3';import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
|
|
||||||
const testConfig: S3ClientConfig = {
|
const testConfig: S3ClientConfig = {
|
||||||
endpoint: 'http://test-minio',
|
endpoint: 'http://test-minio',
|
||||||
@@ -73,7 +79,7 @@ describe('S3Service', () => {
|
|||||||
originalname: 'test-image.jpg',
|
originalname: 'test-image.jpg',
|
||||||
buffer: Buffer.from('test-data'),
|
buffer: Buffer.from('test-data'),
|
||||||
mimetype: 'image/jpeg',
|
mimetype: 'image/jpeg',
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
const userId = 'user-123';
|
const userId = 'user-123';
|
||||||
|
|
||||||
@@ -92,6 +98,82 @@ describe('S3Service', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("listFilesForUser", () => {
|
||||||
|
const userId = "user-123";
|
||||||
|
|
||||||
|
it("should return a correctly formatted list of files for a user", async () => {
|
||||||
|
const mockS3Response = {
|
||||||
|
Contents: [
|
||||||
|
{ Key: `user-${userId}/file1.txt`, LastModified: new Date('2023-01-01') },
|
||||||
|
{ Key: `user-${userId}/image.jpg`, LastModified: new Date('2023-01-02') },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
mockSend.mockResolvedValue(mockS3Response);
|
||||||
|
|
||||||
|
const files = await s3Service.listFilesForUser(userId);
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledOnce();
|
||||||
|
expect(mockSend).toHaveBeenCalledWith(expect.any(ListObjectsV2Command));
|
||||||
|
|
||||||
|
const sentCommand = (mockSend.mock.calls[0][0] as ListObjectsV2Command).input;
|
||||||
|
expect(sentCommand.Bucket).toBe('test-bucket');
|
||||||
|
expect(sentCommand.Prefix).toBe(`user-${userId}/`);
|
||||||
|
|
||||||
|
expect(files).toHaveLength(2);
|
||||||
|
expect(files).toEqual([
|
||||||
|
{ key: `user-${userId}/file1.txt`, lastModified: new Date('2023-01-01') },
|
||||||
|
{ key: `user-${userId}/image.jpg`, lastModified: new Date('2023-01-02') },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty array if the user has no files", async () => {
|
||||||
|
const mockS3Response = {
|
||||||
|
Contents: [],
|
||||||
|
};
|
||||||
|
mockSend.mockResolvedValue(mockS3Response);
|
||||||
|
|
||||||
|
const files = await s3Service.listFilesForUser(userId);
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledOnce();
|
||||||
|
expect(files).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty array if the S3 response has no 'Contents' property", async () => {
|
||||||
|
const mockS3Response = {};
|
||||||
|
mockSend.mockResolvedValue(mockS3Response);
|
||||||
|
|
||||||
|
const files = await s3Service.listFilesForUser(userId);
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledOnce();
|
||||||
|
expect(files).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteFile', () => {
|
||||||
|
it('should call the S3 client with the correct DeleteObjectCommand', async () => {
|
||||||
|
const objectKey = 'user-123/some-file-to-delete.txt';
|
||||||
|
mockSend.mockResolvedValue({});
|
||||||
|
|
||||||
|
await expect(s3Service.deleteFile(objectKey)).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledOnce();
|
||||||
|
expect(mockSend).toHaveBeenCalledWith(expect.any(DeleteObjectCommand));
|
||||||
|
|
||||||
|
const sentCommand = (mockSend.mock.calls[0][0] as DeleteObjectCommand).input;
|
||||||
|
expect(sentCommand.Bucket).toBe('test-bucket');
|
||||||
|
expect(sentCommand.Key).toBe(objectKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the S3 client fails to delete the object', async () => {
|
||||||
|
const objectKey = 'user-123/failing-file.txt';
|
||||||
|
const s3Error = new Error('Access Denied');
|
||||||
|
mockSend.mockRejectedValue(s3Error);
|
||||||
|
|
||||||
|
await expect(s3Service.deleteFile(objectKey)).rejects.toThrow('Access Denied');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('getSignedDownloadUrl', () => {
|
describe('getSignedDownloadUrl', () => {
|
||||||
it('should generate a signed URL for a given object key', async () => {
|
it('should generate a signed URL for a given object key', async () => {
|
||||||
const objectKey = 'user-123/image.png';
|
const objectKey = 'user-123/image.png';
|
||||||
|
|||||||
Reference in New Issue
Block a user