add s3Service rest api

This commit is contained in:
StarAppeal
2025-09-25 02:36:24 +02:00
parent 46b5845140
commit 2e06562af9
6 changed files with 364 additions and 6 deletions
+8
View File
@@ -124,4 +124,12 @@ export const createMockSpotifyApiService = () => ({
export const createMockSpotifyPollingService = () => ({
startPollingForUser: vi.fn(),
stopPollingForUser: vi.fn()
});
export const createMockS3Service = () => ({
ensureBucketExists: vi.fn(),
uploadFile: vi.fn(),
listFilesForUser: vi.fn(),
deleteFile: vi.fn(),
getSignedDownloadUrl: vi.fn(),
});
+151
View File
@@ -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();
});
});
});
+85 -3
View File
@@ -11,8 +11,14 @@ vi.mock('@aws-sdk/client-s3', async (importOriginal) => {
});
import { S3Service, S3ClientConfig } from '../../src/services/s3Service';
import { S3Client, CreateBucketCommand, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import {
S3Client,
CreateBucketCommand,
PutObjectCommand,
GetObjectCommand,
ListObjectsV2Command,
DeleteObjectCommand
} from '@aws-sdk/client-s3';import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const testConfig: S3ClientConfig = {
endpoint: 'http://test-minio',
@@ -73,7 +79,7 @@ describe('S3Service', () => {
originalname: 'test-image.jpg',
buffer: Buffer.from('test-data'),
mimetype: 'image/jpeg',
};
} as any;
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', () => {
it('should generate a signed URL for a given object key', async () => {
const objectKey = 'user-123/image.png';