updated upload unit tests for s3 image handling
This commit is contained in:
@@ -446,15 +446,137 @@ describe('Upload Routes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note: The GET /upload/signed-url/*key route uses Express 5 wildcard syntax
|
describe('GET /upload/signed-url/:key(*)', () => {
|
||||||
// which is not fully compatible with the test environment when mocking.
|
const mockSignedUrl = 'https://bucket.s3.amazonaws.com/messages/uuid.jpg?signature=abc';
|
||||||
// The S3OwnershipService functionality is tested separately in s3OwnershipService.test.js
|
|
||||||
// The route integration is verified in integration tests.
|
beforeEach(() => {
|
||||||
describe('GET /upload/signed-url/*key (wildcard route)', () => {
|
mockGetPresignedDownloadUrl.mockResolvedValue(mockSignedUrl);
|
||||||
it('should be defined as a route', () => {
|
mockCanAccessFile.mockResolvedValue({ authorized: true });
|
||||||
// The route exists and is properly configured
|
});
|
||||||
// Full integration testing of wildcard routes is done in integration tests
|
|
||||||
expect(true).toBe(true);
|
it('should return signed URL for authorized private content (messages)', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/messages/550e8400-e29b-41d4-a716-446655440000.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.url).toBe(mockSignedUrl);
|
||||||
|
expect(response.body.expiresIn).toBe(3600);
|
||||||
|
|
||||||
|
expect(mockCanAccessFile).toHaveBeenCalledWith(
|
||||||
|
'messages/550e8400-e29b-41d4-a716-446655440000.jpg',
|
||||||
|
'user-123'
|
||||||
|
);
|
||||||
|
expect(mockGetPresignedDownloadUrl).toHaveBeenCalledWith(
|
||||||
|
'messages/550e8400-e29b-41d4-a716-446655440000.jpg'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return signed URL for authorized condition-check content', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/condition-checks/550e8400-e29b-41d4-a716-446655440000.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.url).toBe(mockSignedUrl);
|
||||||
|
|
||||||
|
expect(mockCanAccessFile).toHaveBeenCalledWith(
|
||||||
|
'condition-checks/550e8400-e29b-41d4-a716-446655440000.jpg',
|
||||||
|
'user-123'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/messages/uuid.jpg');
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(mockGetPresignedDownloadUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 503 when S3 is disabled', async () => {
|
||||||
|
mockIsEnabled.mockReturnValue(false);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/messages/uuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(503);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for public folder paths (items)', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/items/uuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toBe('Signed URLs only for private content');
|
||||||
|
expect(mockGetPresignedDownloadUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for public folder paths (profiles)', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/profiles/uuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toBe('Signed URLs only for private content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for public folder paths (forum)', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/forum/uuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body.error).toBe('Signed URLs only for private content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 403 when user is not authorized to access file', async () => {
|
||||||
|
mockCanAccessFile.mockResolvedValue({
|
||||||
|
authorized: false,
|
||||||
|
reason: 'Not a participant in this message'
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/messages/uuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body.error).toBe('Access denied');
|
||||||
|
expect(mockGetPresignedDownloadUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle URL-encoded keys', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/messages%2Fuuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
// The key should be decoded
|
||||||
|
expect(mockCanAccessFile).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('messages'),
|
||||||
|
'user-123'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle S3 service errors gracefully', async () => {
|
||||||
|
mockGetPresignedDownloadUrl.mockRejectedValue(new Error('S3 error'));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/messages/uuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ownership service errors gracefully', async () => {
|
||||||
|
mockCanAccessFile.mockRejectedValue(new Error('Database error'));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/upload/signed-url/messages/uuid.jpg')
|
||||||
|
.set('Authorization', 'Bearer valid-token');
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,12 +23,88 @@ jest.mock('../../services/api');
|
|||||||
|
|
||||||
const mockedApi = api as jest.Mocked<typeof api>;
|
const mockedApi = api as jest.Mocked<typeof api>;
|
||||||
|
|
||||||
|
// Mock XMLHttpRequest for uploadToS3 tests
|
||||||
|
class MockXMLHttpRequest {
|
||||||
|
static instances: MockXMLHttpRequest[] = [];
|
||||||
|
|
||||||
|
status = 200;
|
||||||
|
readyState = 4;
|
||||||
|
responseText = '';
|
||||||
|
upload = {
|
||||||
|
onprogress: null as ((e: { lengthComputable: boolean; loaded: number; total: number }) => void) | null,
|
||||||
|
};
|
||||||
|
onload: (() => void) | null = null;
|
||||||
|
onerror: (() => void) | null = null;
|
||||||
|
|
||||||
|
private headers: Record<string, string> = {};
|
||||||
|
private method = '';
|
||||||
|
private url = '';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
MockXMLHttpRequest.instances.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
open(method: string, url: string) {
|
||||||
|
this.method = method;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestHeader(key: string, value: string) {
|
||||||
|
this.headers[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(_data: unknown) {
|
||||||
|
// Use Promise.resolve().then for async behavior in tests
|
||||||
|
// This allows promises to resolve without real delays
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
if (this.upload.onprogress) {
|
||||||
|
this.upload.onprogress({ lengthComputable: true, loaded: 50, total: 100 });
|
||||||
|
this.upload.onprogress({ lengthComputable: true, loaded: 100, total: 100 });
|
||||||
|
}
|
||||||
|
if (this.onload) {
|
||||||
|
this.onload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeaders() {
|
||||||
|
return this.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMethod() {
|
||||||
|
return this.method;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl() {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
static reset() {
|
||||||
|
MockXMLHttpRequest.instances = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static getLastInstance() {
|
||||||
|
return MockXMLHttpRequest.instances[MockXMLHttpRequest.instances.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original XMLHttpRequest
|
||||||
|
const originalXMLHttpRequest = global.XMLHttpRequest;
|
||||||
|
|
||||||
describe('Upload Service', () => {
|
describe('Upload Service', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
MockXMLHttpRequest.reset();
|
||||||
// Reset environment variables
|
// Reset environment variables
|
||||||
process.env.REACT_APP_S3_BUCKET = 'test-bucket';
|
process.env.REACT_APP_S3_BUCKET = 'test-bucket';
|
||||||
process.env.REACT_APP_AWS_REGION = 'us-east-1';
|
process.env.REACT_APP_AWS_REGION = 'us-east-1';
|
||||||
|
// Mock XMLHttpRequest globally
|
||||||
|
(global as unknown as { XMLHttpRequest: typeof MockXMLHttpRequest }).XMLHttpRequest = MockXMLHttpRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original XMLHttpRequest
|
||||||
|
(global as unknown as { XMLHttpRequest: typeof XMLHttpRequest }).XMLHttpRequest = originalXMLHttpRequest;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getPublicImageUrl', () => {
|
describe('getPublicImageUrl', () => {
|
||||||
@@ -173,18 +249,42 @@ describe('Upload Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('uploadToS3', () => {
|
describe('uploadToS3', () => {
|
||||||
// Note: XMLHttpRequest mocking is complex and can cause timeouts.
|
const mockFile = new File(['test content'], 'photo.jpg', { type: 'image/jpeg' });
|
||||||
// The uploadToS3 function is a thin wrapper around XHR.
|
const mockUploadUrl = 'https://presigned-url.s3.amazonaws.com/items/uuid.jpg?signature=abc';
|
||||||
// Testing focuses on verifying the function signature and basic behavior.
|
|
||||||
|
|
||||||
it('should export uploadToS3 function', () => {
|
it('should upload file successfully', async () => {
|
||||||
expect(typeof uploadToS3).toBe('function');
|
await uploadToS3(mockFile, mockUploadUrl);
|
||||||
|
|
||||||
|
const instance = MockXMLHttpRequest.getLastInstance();
|
||||||
|
expect(instance.getMethod()).toBe('PUT');
|
||||||
|
expect(instance.getUrl()).toBe(mockUploadUrl);
|
||||||
|
expect(instance.getHeaders()['Content-Type']).toBe('image/jpeg');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should accept file, url, and options parameters', () => {
|
it('should call onProgress callback during upload', async () => {
|
||||||
// Verify function signature
|
const onProgress = jest.fn();
|
||||||
|
|
||||||
|
await uploadToS3(mockFile, mockUploadUrl, { onProgress });
|
||||||
|
|
||||||
|
// Progress should be called at least once
|
||||||
|
expect(onProgress).toHaveBeenCalled();
|
||||||
|
// Should receive percentage values
|
||||||
|
expect(onProgress).toHaveBeenCalledWith(expect.any(Number));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export uploadToS3 function with correct signature', () => {
|
||||||
|
expect(typeof uploadToS3).toBe('function');
|
||||||
|
// Function accepts file, url, and optional options
|
||||||
expect(uploadToS3.length).toBeGreaterThanOrEqual(2);
|
expect(uploadToS3.length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set correct content-type header', async () => {
|
||||||
|
const pngFile = new File(['test'], 'image.png', { type: 'image/png' });
|
||||||
|
await uploadToS3(pngFile, mockUploadUrl);
|
||||||
|
|
||||||
|
const instance = MockXMLHttpRequest.getLastInstance();
|
||||||
|
expect(instance.getHeaders()['Content-Type']).toBe('image/png');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('confirmUploads', () => {
|
describe('confirmUploads', () => {
|
||||||
@@ -214,70 +314,230 @@ describe('Upload Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('uploadFile', () => {
|
describe('uploadFile', () => {
|
||||||
it('should call getPresignedUrl and confirmUploads in sequence', async () => {
|
const mockFile = new File(['test content'], 'photo.jpg', { type: 'image/jpeg' });
|
||||||
// Test the flow without mocking XMLHttpRequest (which is complex)
|
const presignResponse: PresignedUrlResponse = {
|
||||||
// Instead test that the functions are called with correct parameters
|
uploadUrl: 'https://presigned.s3.amazonaws.com/items/uuid.jpg',
|
||||||
const file = new File(['test'], 'photo.jpg', { type: 'image/jpeg' });
|
key: 'items/uuid.jpg',
|
||||||
const presignResponse: PresignedUrlResponse = {
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid.jpg',
|
||||||
uploadUrl: 'https://presigned.s3.amazonaws.com',
|
expiresAt: new Date().toISOString(),
|
||||||
key: 'items/uuid.jpg',
|
};
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid.jpg',
|
|
||||||
expiresAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
it('should complete full upload flow successfully', async () => {
|
||||||
|
// Mock presign response
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
|
mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
|
||||||
|
// Mock confirm response
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: { confirmed: [presignResponse.key], total: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
// Just test getPresignedUrl is called correctly
|
const result = await uploadFile('item', mockFile);
|
||||||
await getPresignedUrl('item', file);
|
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
key: presignResponse.key,
|
||||||
|
publicUrl: presignResponse.publicUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify presign was called
|
||||||
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', {
|
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', {
|
||||||
uploadType: 'item',
|
uploadType: 'item',
|
||||||
contentType: 'image/jpeg',
|
contentType: 'image/jpeg',
|
||||||
fileName: 'photo.jpg',
|
fileName: 'photo.jpg',
|
||||||
fileSize: file.size,
|
fileSize: mockFile.size,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Verify confirm was called
|
||||||
|
expect(mockedApi.post).toHaveBeenCalledWith('/upload/confirm', {
|
||||||
|
keys: [presignResponse.key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when upload verification fails', async () => {
|
||||||
|
mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
|
||||||
|
// Mock confirm returning empty confirmed array
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: { confirmed: [], total: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(uploadFile('item', mockFile)).rejects.toThrow('Upload verification failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass onProgress to uploadToS3', async () => {
|
||||||
|
const onProgress = jest.fn();
|
||||||
|
|
||||||
|
mockedApi.post.mockResolvedValueOnce({ data: presignResponse });
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: { confirmed: [presignResponse.key], total: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
await uploadFile('item', mockFile, { onProgress });
|
||||||
|
|
||||||
|
// onProgress should have been called during XHR upload
|
||||||
|
expect(onProgress).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different upload types', async () => {
|
||||||
|
const messagePresignResponse = {
|
||||||
|
...presignResponse,
|
||||||
|
key: 'messages/uuid.jpg',
|
||||||
|
publicUrl: null, // Messages are private
|
||||||
|
};
|
||||||
|
|
||||||
|
mockedApi.post.mockResolvedValueOnce({ data: messagePresignResponse });
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: { confirmed: [messagePresignResponse.key], total: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await uploadFile('message', mockFile);
|
||||||
|
|
||||||
|
expect(result.key).toBe('messages/uuid.jpg');
|
||||||
|
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign', expect.objectContaining({
|
||||||
|
uploadType: 'message',
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('uploadFiles', () => {
|
describe('uploadFiles', () => {
|
||||||
|
const mockFiles = [
|
||||||
|
new File(['test1'], 'photo1.jpg', { type: 'image/jpeg' }),
|
||||||
|
new File(['test2'], 'photo2.png', { type: 'image/png' }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const presignResponses: PresignedUrlResponse[] = [
|
||||||
|
{
|
||||||
|
uploadUrl: 'https://presigned1.s3.amazonaws.com/items/uuid1.jpg',
|
||||||
|
key: 'items/uuid1.jpg',
|
||||||
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
||||||
|
expiresAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uploadUrl: 'https://presigned2.s3.amazonaws.com/items/uuid2.png',
|
||||||
|
key: 'items/uuid2.png',
|
||||||
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
||||||
|
expiresAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
it('should return empty array for empty files array', async () => {
|
it('should return empty array for empty files array', async () => {
|
||||||
const result = await uploadFiles('item', []);
|
const result = await uploadFiles('item', []);
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getPresignedUrls with correct parameters', async () => {
|
it('should complete full batch upload flow successfully', async () => {
|
||||||
const files = [
|
|
||||||
new File(['test1'], 'photo1.jpg', { type: 'image/jpeg' }),
|
|
||||||
new File(['test2'], 'photo2.png', { type: 'image/png' }),
|
|
||||||
];
|
|
||||||
|
|
||||||
const presignResponses: PresignedUrlResponse[] = [
|
|
||||||
{
|
|
||||||
uploadUrl: 'https://presigned1.s3.amazonaws.com',
|
|
||||||
key: 'items/uuid1.jpg',
|
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
|
||||||
expiresAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uploadUrl: 'https://presigned2.s3.amazonaws.com',
|
|
||||||
key: 'items/uuid2.png',
|
|
||||||
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
|
||||||
expiresAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
confirmed: presignResponses.map((p) => p.key),
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await getPresignedUrls('item', files);
|
const result = await uploadFiles('item', mockFiles);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
key: 'items/uuid1.jpg',
|
||||||
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid1.jpg',
|
||||||
|
});
|
||||||
|
expect(result[1]).toEqual({
|
||||||
|
key: 'items/uuid2.png',
|
||||||
|
publicUrl: 'https://bucket.s3.amazonaws.com/items/uuid2.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify batch presign was called
|
||||||
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', {
|
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', {
|
||||||
uploadType: 'item',
|
uploadType: 'item',
|
||||||
files: [
|
files: [
|
||||||
{ contentType: 'image/jpeg', fileName: 'photo1.jpg', fileSize: files[0].size },
|
{ contentType: 'image/jpeg', fileName: 'photo1.jpg', fileSize: mockFiles[0].size },
|
||||||
{ contentType: 'image/png', fileName: 'photo2.png', fileSize: files[1].size },
|
{ contentType: 'image/png', fileName: 'photo2.png', fileSize: mockFiles[1].size },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Verify confirm was called with all keys
|
||||||
|
expect(mockedApi.post).toHaveBeenCalledWith('/upload/confirm', {
|
||||||
|
keys: ['items/uuid1.jpg', 'items/uuid2.png'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out unconfirmed uploads', async () => {
|
||||||
|
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
||||||
|
// Only first file confirmed
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
confirmed: ['items/uuid1.jpg'],
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
|
const result = await uploadFiles('item', mockFiles);
|
||||||
|
|
||||||
|
// Only confirmed uploads should be returned
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].key).toBe('items/uuid1.jpg');
|
||||||
|
|
||||||
|
// Should log warning about failed verification
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('1 uploads failed verification');
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all uploads failing verification', async () => {
|
||||||
|
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
confirmed: [],
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||||
|
|
||||||
|
const result = await uploadFiles('item', mockFiles);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('2 uploads failed verification');
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upload all files in parallel', async () => {
|
||||||
|
mockedApi.post.mockResolvedValueOnce({ data: { uploads: presignResponses } });
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
confirmed: presignResponses.map((p) => p.key),
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await uploadFiles('item', mockFiles);
|
||||||
|
|
||||||
|
// Should have created 2 XHR instances for parallel uploads
|
||||||
|
expect(MockXMLHttpRequest.instances.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different upload types', async () => {
|
||||||
|
const forumResponses = presignResponses.map((r) => ({
|
||||||
|
...r,
|
||||||
|
key: r.key.replace('items/', 'forum/'),
|
||||||
|
publicUrl: r.publicUrl.replace('items/', 'forum/'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
mockedApi.post.mockResolvedValueOnce({ data: { uploads: forumResponses } });
|
||||||
|
mockedApi.post.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
confirmed: forumResponses.map((p) => p.key),
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await uploadFiles('forum', mockFiles);
|
||||||
|
|
||||||
|
expect(result[0].key).toBe('forum/uuid1.jpg');
|
||||||
|
expect(mockedApi.post).toHaveBeenCalledWith('/upload/presign-batch', expect.objectContaining({
|
||||||
|
uploadType: 'forum',
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user