Added Documents Feature
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* @ai-summary Integration tests for Documents API endpoints
|
||||
* @ai-context Tests full API flow with auth, database, and storage
|
||||
*/
|
||||
|
||||
import request from 'supertest';
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { build } from '../../../../app';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
describe('Documents Integration Tests', () => {
|
||||
let app: FastifyInstance;
|
||||
let testUserId: string;
|
||||
let testVehicleId: string;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = build({ logger: false });
|
||||
await app.ready();
|
||||
|
||||
// Create test user context
|
||||
testUserId = 'test-user-' + Date.now();
|
||||
authToken = 'Bearer test-token';
|
||||
|
||||
// Create test vehicle for document association
|
||||
const vehicleData = {
|
||||
vin: '1HGBH41JXMN109186',
|
||||
nickname: 'Test Car',
|
||||
color: 'Blue',
|
||||
odometerReading: 50000,
|
||||
};
|
||||
|
||||
const vehicleResponse = await request(app.server)
|
||||
.post('/api/vehicles')
|
||||
.set('Authorization', authToken)
|
||||
.send(vehicleData);
|
||||
|
||||
testVehicleId = vehicleResponse.body.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('POST /api/documents', () => {
|
||||
it('should create document metadata', async () => {
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'insurance',
|
||||
title: 'Car Insurance Policy',
|
||||
notes: 'Annual policy',
|
||||
details: { provider: 'State Farm', policy_number: '12345' },
|
||||
issued_date: '2024-01-01',
|
||||
expiration_date: '2024-12-31',
|
||||
};
|
||||
|
||||
const response = await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.send(documentData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
user_id: testUserId,
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'insurance',
|
||||
title: 'Car Insurance Policy',
|
||||
notes: 'Annual policy',
|
||||
details: { provider: 'State Farm', policy_number: '12345' },
|
||||
issued_date: '2024-01-01',
|
||||
expiration_date: '2024-12-31',
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
});
|
||||
|
||||
// Storage fields should be null initially
|
||||
expect(response.body.storage_bucket).toBeNull();
|
||||
expect(response.body.storage_key).toBeNull();
|
||||
expect(response.body.file_name).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject document for non-existent vehicle', async () => {
|
||||
const documentData = {
|
||||
vehicle_id: 'non-existent-vehicle',
|
||||
document_type: 'registration',
|
||||
title: 'Invalid Document',
|
||||
};
|
||||
|
||||
await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.send(documentData)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'insurance',
|
||||
title: 'Unauthorized Document',
|
||||
};
|
||||
|
||||
await request(app.server)
|
||||
.post('/api/documents')
|
||||
.send(documentData)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/documents', () => {
|
||||
let testDocumentId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test document
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'registration',
|
||||
title: 'Test Document for Listing',
|
||||
};
|
||||
|
||||
const response = await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.send(documentData);
|
||||
|
||||
testDocumentId = response.body.id;
|
||||
});
|
||||
|
||||
it('should list user documents', async () => {
|
||||
const response = await request(app.server)
|
||||
.get('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
expect(response.body.some((doc: any) => doc.id === testDocumentId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter documents by vehicle', async () => {
|
||||
const response = await request(app.server)
|
||||
.get('/api/documents')
|
||||
.query({ vehicleId: testVehicleId })
|
||||
.set('Authorization', authToken)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
response.body.forEach((doc: any) => {
|
||||
expect(doc.vehicle_id).toBe(testVehicleId);
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter documents by type', async () => {
|
||||
const response = await request(app.server)
|
||||
.get('/api/documents')
|
||||
.query({ type: 'registration' })
|
||||
.set('Authorization', authToken)
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
response.body.forEach((doc: any) => {
|
||||
expect(doc.document_type).toBe('registration');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/documents/:id', () => {
|
||||
let testDocumentId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'insurance',
|
||||
title: 'Single Document Test',
|
||||
};
|
||||
|
||||
const response = await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.send(documentData);
|
||||
|
||||
testDocumentId = response.body.id;
|
||||
});
|
||||
|
||||
it('should get single document', async () => {
|
||||
const response = await request(app.server)
|
||||
.get(`/api/documents/${testDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: testDocumentId,
|
||||
user_id: testUserId,
|
||||
vehicle_id: testVehicleId,
|
||||
title: 'Single Document Test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent document', async () => {
|
||||
await request(app.server)
|
||||
.get('/api/documents/non-existent-id')
|
||||
.set('Authorization', authToken)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /api/documents/:id', () => {
|
||||
let testDocumentId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'insurance',
|
||||
title: 'Document to Update',
|
||||
notes: 'Original notes',
|
||||
};
|
||||
|
||||
const response = await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.send(documentData);
|
||||
|
||||
testDocumentId = response.body.id;
|
||||
});
|
||||
|
||||
it('should update document metadata', async () => {
|
||||
const updateData = {
|
||||
title: 'Updated Document Title',
|
||||
notes: 'Updated notes',
|
||||
details: { updated: true },
|
||||
};
|
||||
|
||||
const response = await request(app.server)
|
||||
.put(`/api/documents/${testDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.send(updateData)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: testDocumentId,
|
||||
title: 'Updated Document Title',
|
||||
notes: 'Updated notes',
|
||||
details: { updated: true },
|
||||
updated_at: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent document', async () => {
|
||||
await request(app.server)
|
||||
.put('/api/documents/non-existent-id')
|
||||
.set('Authorization', authToken)
|
||||
.send({ title: 'New Title' })
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('File Upload/Download Flow', () => {
|
||||
let testDocumentId: string;
|
||||
let testFilePath: string;
|
||||
|
||||
beforeAll(() => {
|
||||
// Create test file
|
||||
testFilePath = path.join(__dirname, 'test-file.pdf');
|
||||
fs.writeFileSync(testFilePath, 'Fake PDF content for testing');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test file
|
||||
if (fs.existsSync(testFilePath)) {
|
||||
fs.unlinkSync(testFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'insurance',
|
||||
title: 'Document for Upload Test',
|
||||
};
|
||||
|
||||
const response = await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.send(documentData);
|
||||
|
||||
testDocumentId = response.body.id;
|
||||
});
|
||||
|
||||
it('should upload file to document', async () => {
|
||||
const response = await request(app.server)
|
||||
.post(`/api/documents/${testDocumentId}/upload`)
|
||||
.set('Authorization', authToken)
|
||||
.attach('file', testFilePath)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: testDocumentId,
|
||||
storage_bucket: expect.any(String),
|
||||
storage_key: expect.any(String),
|
||||
file_name: 'test-file.pdf',
|
||||
content_type: expect.any(String),
|
||||
file_size: expect.any(Number),
|
||||
});
|
||||
|
||||
expect(response.body.storage_key).toMatch(/^documents\//);
|
||||
});
|
||||
|
||||
it('should reject unsupported file types', async () => {
|
||||
// Create temporary executable file
|
||||
const execPath = path.join(__dirname, 'test.exe');
|
||||
fs.writeFileSync(execPath, 'fake executable');
|
||||
|
||||
try {
|
||||
await request(app.server)
|
||||
.post(`/api/documents/${testDocumentId}/upload`)
|
||||
.set('Authorization', authToken)
|
||||
.attach('file', execPath)
|
||||
.expect(415);
|
||||
} finally {
|
||||
if (fs.existsSync(execPath)) {
|
||||
fs.unlinkSync(execPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should download uploaded file', async () => {
|
||||
// First upload a file
|
||||
await request(app.server)
|
||||
.post(`/api/documents/${testDocumentId}/upload`)
|
||||
.set('Authorization', authToken)
|
||||
.attach('file', testFilePath);
|
||||
|
||||
// Then download it
|
||||
const response = await request(app.server)
|
||||
.get(`/api/documents/${testDocumentId}/download`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers['content-disposition']).toContain('test-file.pdf');
|
||||
expect(response.body.toString()).toBe('Fake PDF content for testing');
|
||||
});
|
||||
|
||||
it('should return 404 for download without uploaded file', async () => {
|
||||
await request(app.server)
|
||||
.get(`/api/documents/${testDocumentId}/download`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/documents/:id', () => {
|
||||
let testDocumentId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'registration',
|
||||
title: 'Document to Delete',
|
||||
};
|
||||
|
||||
const response = await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', authToken)
|
||||
.send(documentData);
|
||||
|
||||
testDocumentId = response.body.id;
|
||||
});
|
||||
|
||||
it('should soft delete document', async () => {
|
||||
await request(app.server)
|
||||
.delete(`/api/documents/${testDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(204);
|
||||
|
||||
// Verify document is no longer accessible
|
||||
await request(app.server)
|
||||
.get(`/api/documents/${testDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return 404 for already deleted document', async () => {
|
||||
// Delete once
|
||||
await request(app.server)
|
||||
.delete(`/api/documents/${testDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(204);
|
||||
|
||||
// Try to delete again
|
||||
await request(app.server)
|
||||
.delete(`/api/documents/${testDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(204); // Idempotent behavior
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authorization and Ownership', () => {
|
||||
let otherUserDocumentId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create document as different user
|
||||
const documentData = {
|
||||
vehicle_id: testVehicleId,
|
||||
document_type: 'insurance',
|
||||
title: 'Other User Document',
|
||||
};
|
||||
|
||||
// Mock different user context
|
||||
const otherUserToken = 'Bearer other-user-token';
|
||||
const response = await request(app.server)
|
||||
.post('/api/documents')
|
||||
.set('Authorization', otherUserToken)
|
||||
.send(documentData);
|
||||
|
||||
otherUserDocumentId = response.body.id;
|
||||
});
|
||||
|
||||
it('should not allow access to other users documents', async () => {
|
||||
await request(app.server)
|
||||
.get(`/api/documents/${otherUserDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should not allow update of other users documents', async () => {
|
||||
await request(app.server)
|
||||
.put(`/api/documents/${otherUserDocumentId}`)
|
||||
.set('Authorization', authToken)
|
||||
.send({ title: 'Hacked Title' })
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* @ai-summary Unit tests for DocumentsRepository
|
||||
* @ai-context Tests database layer with mocked pool
|
||||
*/
|
||||
|
||||
import { DocumentsRepository } from '../../data/documents.repository';
|
||||
import type { Pool } from 'pg';
|
||||
|
||||
describe('DocumentsRepository', () => {
|
||||
let repository: DocumentsRepository;
|
||||
let mockPool: jest.Mocked<Pool>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPool = {
|
||||
query: jest.fn(),
|
||||
} as any;
|
||||
repository = new DocumentsRepository(mockPool);
|
||||
});
|
||||
|
||||
describe('insert', () => {
|
||||
const mockDocumentData = {
|
||||
id: 'doc-123',
|
||||
user_id: 'user-123',
|
||||
vehicle_id: 'vehicle-123',
|
||||
document_type: 'insurance' as const,
|
||||
title: 'Test Document',
|
||||
notes: 'Test notes',
|
||||
details: { provider: 'Test Provider' },
|
||||
issued_date: '2024-01-01',
|
||||
expiration_date: '2024-12-31',
|
||||
};
|
||||
|
||||
it('should insert document with all fields', async () => {
|
||||
const mockResult = { rows: [{ ...mockDocumentData, created_at: '2024-01-01T00:00:00Z' }] };
|
||||
mockPool.query.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await repository.insert(mockDocumentData);
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO documents'),
|
||||
[
|
||||
'doc-123',
|
||||
'user-123',
|
||||
'vehicle-123',
|
||||
'insurance',
|
||||
'Test Document',
|
||||
'Test notes',
|
||||
{ provider: 'Test Provider' },
|
||||
'2024-01-01',
|
||||
'2024-12-31',
|
||||
]
|
||||
);
|
||||
expect(result).toEqual(mockResult.rows[0]);
|
||||
});
|
||||
|
||||
it('should insert document with null optional fields', async () => {
|
||||
const minimalData = {
|
||||
id: 'doc-123',
|
||||
user_id: 'user-123',
|
||||
vehicle_id: 'vehicle-123',
|
||||
document_type: 'registration' as const,
|
||||
title: 'Test Document',
|
||||
};
|
||||
const mockResult = { rows: [{ ...minimalData, notes: null, details: null }] };
|
||||
mockPool.query.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await repository.insert(minimalData);
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO documents'),
|
||||
[
|
||||
'doc-123',
|
||||
'user-123',
|
||||
'vehicle-123',
|
||||
'registration',
|
||||
'Test Document',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
);
|
||||
expect(result).toEqual(mockResult.rows[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should find document by id and user', async () => {
|
||||
const mockDocument = { id: 'doc-123', user_id: 'user-123', title: 'Test' };
|
||||
mockPool.query.mockResolvedValue({ rows: [mockDocument] });
|
||||
|
||||
const result = await repository.findById('doc-123', 'user-123');
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM documents WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL',
|
||||
['doc-123', 'user-123']
|
||||
);
|
||||
expect(result).toEqual(mockDocument);
|
||||
});
|
||||
|
||||
it('should return null if document not found', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [] });
|
||||
|
||||
const result = await repository.findById('doc-123', 'user-123');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listByUser', () => {
|
||||
const mockDocuments = [
|
||||
{ id: 'doc-1', user_id: 'user-123', title: 'Doc 1' },
|
||||
{ id: 'doc-2', user_id: 'user-123', title: 'Doc 2' },
|
||||
];
|
||||
|
||||
it('should list all user documents without filters', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: mockDocuments });
|
||||
|
||||
const result = await repository.listByUser('user-123');
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM documents WHERE user_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC',
|
||||
['user-123']
|
||||
);
|
||||
expect(result).toEqual(mockDocuments);
|
||||
});
|
||||
|
||||
it('should list documents with vehicleId filter', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [mockDocuments[0]] });
|
||||
|
||||
const result = await repository.listByUser('user-123', { vehicleId: 'vehicle-123' });
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM documents WHERE user_id = $1 AND deleted_at IS NULL AND vehicle_id = $2 ORDER BY created_at DESC',
|
||||
['user-123', 'vehicle-123']
|
||||
);
|
||||
expect(result).toEqual([mockDocuments[0]]);
|
||||
});
|
||||
|
||||
it('should list documents with type filter', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [mockDocuments[0]] });
|
||||
|
||||
const result = await repository.listByUser('user-123', { type: 'insurance' });
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM documents WHERE user_id = $1 AND deleted_at IS NULL AND document_type = $2 ORDER BY created_at DESC',
|
||||
['user-123', 'insurance']
|
||||
);
|
||||
expect(result).toEqual([mockDocuments[0]]);
|
||||
});
|
||||
|
||||
it('should list documents with expiresBefore filter', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [mockDocuments[0]] });
|
||||
|
||||
const result = await repository.listByUser('user-123', { expiresBefore: '2024-12-31' });
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM documents WHERE user_id = $1 AND deleted_at IS NULL AND expiration_date <= $2 ORDER BY created_at DESC',
|
||||
['user-123', '2024-12-31']
|
||||
);
|
||||
expect(result).toEqual([mockDocuments[0]]);
|
||||
});
|
||||
|
||||
it('should list documents with multiple filters', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [mockDocuments[0]] });
|
||||
|
||||
const result = await repository.listByUser('user-123', {
|
||||
vehicleId: 'vehicle-123',
|
||||
type: 'insurance',
|
||||
expiresBefore: '2024-12-31',
|
||||
});
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM documents WHERE user_id = $1 AND deleted_at IS NULL AND vehicle_id = $2 AND document_type = $3 AND expiration_date <= $4 ORDER BY created_at DESC',
|
||||
['user-123', 'vehicle-123', 'insurance', '2024-12-31']
|
||||
);
|
||||
expect(result).toEqual([mockDocuments[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('softDelete', () => {
|
||||
it('should soft delete document', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [] });
|
||||
|
||||
await repository.softDelete('doc-123', 'user-123');
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'UPDATE documents SET deleted_at = NOW() WHERE id = $1 AND user_id = $2',
|
||||
['doc-123', 'user-123']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMetadata', () => {
|
||||
it('should update single field', async () => {
|
||||
const mockUpdated = { id: 'doc-123', title: 'Updated Title' };
|
||||
mockPool.query.mockResolvedValue({ rows: [mockUpdated] });
|
||||
|
||||
const result = await repository.updateMetadata('doc-123', 'user-123', { title: 'Updated Title' });
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'UPDATE documents SET title = $1 WHERE id = $2 AND user_id = $3 AND deleted_at IS NULL RETURNING *',
|
||||
['Updated Title', 'doc-123', 'user-123']
|
||||
);
|
||||
expect(result).toEqual(mockUpdated);
|
||||
});
|
||||
|
||||
it('should update multiple fields', async () => {
|
||||
const mockUpdated = { id: 'doc-123', title: 'Updated Title', notes: 'Updated notes' };
|
||||
mockPool.query.mockResolvedValue({ rows: [mockUpdated] });
|
||||
|
||||
const result = await repository.updateMetadata('doc-123', 'user-123', {
|
||||
title: 'Updated Title',
|
||||
notes: 'Updated notes',
|
||||
details: { key: 'value' },
|
||||
});
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'UPDATE documents SET title = $1, notes = $2, details = $3 WHERE id = $4 AND user_id = $5 AND deleted_at IS NULL RETURNING *',
|
||||
['Updated Title', 'Updated notes', { key: 'value' }, 'doc-123', 'user-123']
|
||||
);
|
||||
expect(result).toEqual(mockUpdated);
|
||||
});
|
||||
|
||||
it('should handle null values', async () => {
|
||||
const mockUpdated = { id: 'doc-123', notes: null };
|
||||
mockPool.query.mockResolvedValue({ rows: [mockUpdated] });
|
||||
|
||||
const result = await repository.updateMetadata('doc-123', 'user-123', { notes: null });
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'UPDATE documents SET notes = $1 WHERE id = $2 AND user_id = $3 AND deleted_at IS NULL RETURNING *',
|
||||
[null, 'doc-123', 'user-123']
|
||||
);
|
||||
expect(result).toEqual(mockUpdated);
|
||||
});
|
||||
|
||||
it('should return existing record if no fields to update', async () => {
|
||||
const mockExisting = { id: 'doc-123', title: 'Existing' };
|
||||
mockPool.query.mockResolvedValue({ rows: [mockExisting] });
|
||||
|
||||
const result = await repository.updateMetadata('doc-123', 'user-123', {});
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM documents WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL',
|
||||
['doc-123', 'user-123']
|
||||
);
|
||||
expect(result).toEqual(mockExisting);
|
||||
});
|
||||
|
||||
it('should return null if document not found', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [] });
|
||||
|
||||
const result = await repository.updateMetadata('doc-123', 'user-123', { title: 'New Title' });
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStorageMeta', () => {
|
||||
it('should update storage metadata', async () => {
|
||||
const storageMeta = {
|
||||
storage_bucket: 'test-bucket',
|
||||
storage_key: 'test-key',
|
||||
file_name: 'test.pdf',
|
||||
content_type: 'application/pdf',
|
||||
file_size: 1024,
|
||||
file_hash: 'hash123',
|
||||
};
|
||||
const mockUpdated = { id: 'doc-123', ...storageMeta };
|
||||
mockPool.query.mockResolvedValue({ rows: [mockUpdated] });
|
||||
|
||||
const result = await repository.updateStorageMeta('doc-123', 'user-123', storageMeta);
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE documents SET'),
|
||||
[
|
||||
'test-bucket',
|
||||
'test-key',
|
||||
'test.pdf',
|
||||
'application/pdf',
|
||||
1024,
|
||||
'hash123',
|
||||
'doc-123',
|
||||
'user-123',
|
||||
]
|
||||
);
|
||||
expect(result).toEqual(mockUpdated);
|
||||
});
|
||||
|
||||
it('should handle null file_hash', async () => {
|
||||
const storageMeta = {
|
||||
storage_bucket: 'test-bucket',
|
||||
storage_key: 'test-key',
|
||||
file_name: 'test.pdf',
|
||||
content_type: 'application/pdf',
|
||||
file_size: 1024,
|
||||
};
|
||||
mockPool.query.mockResolvedValue({ rows: [{ id: 'doc-123', ...storageMeta, file_hash: null }] });
|
||||
|
||||
const result = await repository.updateStorageMeta('doc-123', 'user-123', storageMeta);
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE documents SET'),
|
||||
[
|
||||
'test-bucket',
|
||||
'test-key',
|
||||
'test.pdf',
|
||||
'application/pdf',
|
||||
1024,
|
||||
null,
|
||||
'doc-123',
|
||||
'user-123',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null if document not found', async () => {
|
||||
const storageMeta = {
|
||||
storage_bucket: 'test-bucket',
|
||||
storage_key: 'test-key',
|
||||
file_name: 'test.pdf',
|
||||
content_type: 'application/pdf',
|
||||
file_size: 1024,
|
||||
};
|
||||
mockPool.query.mockResolvedValue({ rows: [] });
|
||||
|
||||
const result = await repository.updateStorageMeta('doc-123', 'user-123', storageMeta);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* @ai-summary Unit tests for DocumentsService
|
||||
* @ai-context Tests business logic with mocked dependencies
|
||||
*/
|
||||
|
||||
import { DocumentsService } from '../../domain/documents.service';
|
||||
import { DocumentsRepository } from '../../data/documents.repository';
|
||||
import pool from '../../../../core/config/database';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../data/documents.repository');
|
||||
jest.mock('../../../../core/config/database');
|
||||
|
||||
const mockRepository = jest.mocked(DocumentsRepository);
|
||||
const mockPool = jest.mocked(pool);
|
||||
|
||||
describe('DocumentsService', () => {
|
||||
let service: DocumentsService;
|
||||
let repositoryInstance: jest.Mocked<DocumentsRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
repositoryInstance = {
|
||||
insert: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
listByUser: jest.fn(),
|
||||
updateMetadata: jest.fn(),
|
||||
updateStorageMeta: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockRepository.mockImplementation(() => repositoryInstance);
|
||||
service = new DocumentsService();
|
||||
});
|
||||
|
||||
describe('createDocument', () => {
|
||||
const mockDocumentBody = {
|
||||
vehicle_id: 'vehicle-123',
|
||||
document_type: 'insurance' as const,
|
||||
title: 'Car Insurance Policy',
|
||||
notes: 'Annual insurance policy',
|
||||
details: { provider: 'State Farm' },
|
||||
issued_date: '2024-01-01',
|
||||
expiration_date: '2024-12-31',
|
||||
};
|
||||
|
||||
const mockCreatedDocument = {
|
||||
id: 'doc-123',
|
||||
user_id: 'user-123',
|
||||
vehicle_id: 'vehicle-123',
|
||||
document_type: 'insurance' as const,
|
||||
title: 'Car Insurance Policy',
|
||||
notes: 'Annual insurance policy',
|
||||
details: { provider: 'State Farm' },
|
||||
storage_bucket: null,
|
||||
storage_key: null,
|
||||
file_name: null,
|
||||
content_type: null,
|
||||
file_size: null,
|
||||
file_hash: null,
|
||||
issued_date: '2024-01-01',
|
||||
expiration_date: '2024-12-31',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
deleted_at: null,
|
||||
};
|
||||
|
||||
it('should create a document successfully', async () => {
|
||||
// Mock vehicle ownership check
|
||||
mockPool.query.mockResolvedValue({ rows: [{ id: 'vehicle-123' }] });
|
||||
repositoryInstance.insert.mockResolvedValue(mockCreatedDocument);
|
||||
|
||||
const result = await service.createDocument('user-123', mockDocumentBody);
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2',
|
||||
['vehicle-123', 'user-123']
|
||||
);
|
||||
expect(repositoryInstance.insert).toHaveBeenCalledWith({
|
||||
id: expect.any(String),
|
||||
user_id: 'user-123',
|
||||
vehicle_id: 'vehicle-123',
|
||||
document_type: 'insurance',
|
||||
title: 'Car Insurance Policy',
|
||||
notes: 'Annual insurance policy',
|
||||
details: { provider: 'State Farm' },
|
||||
issued_date: '2024-01-01',
|
||||
expiration_date: '2024-12-31',
|
||||
});
|
||||
expect(result).toEqual(mockCreatedDocument);
|
||||
});
|
||||
|
||||
it('should create document with minimal data', async () => {
|
||||
const minimalBody = {
|
||||
vehicle_id: 'vehicle-123',
|
||||
document_type: 'registration' as const,
|
||||
title: 'Vehicle Registration',
|
||||
};
|
||||
|
||||
mockPool.query.mockResolvedValue({ rows: [{ id: 'vehicle-123' }] });
|
||||
repositoryInstance.insert.mockResolvedValue({
|
||||
...mockCreatedDocument,
|
||||
document_type: 'registration',
|
||||
title: 'Vehicle Registration',
|
||||
notes: null,
|
||||
details: null,
|
||||
issued_date: null,
|
||||
expiration_date: null,
|
||||
});
|
||||
|
||||
const result = await service.createDocument('user-123', minimalBody);
|
||||
|
||||
expect(repositoryInstance.insert).toHaveBeenCalledWith({
|
||||
id: expect.any(String),
|
||||
user_id: 'user-123',
|
||||
vehicle_id: 'vehicle-123',
|
||||
document_type: 'registration',
|
||||
title: 'Vehicle Registration',
|
||||
notes: null,
|
||||
details: null,
|
||||
issued_date: null,
|
||||
expiration_date: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject document for non-owned vehicle', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [] });
|
||||
|
||||
await expect(service.createDocument('user-123', mockDocumentBody))
|
||||
.rejects.toThrow('Vehicle not found or not owned by user');
|
||||
|
||||
expect(mockPool.query).toHaveBeenCalledWith(
|
||||
'SELECT id FROM vehicles WHERE id = $1 AND user_id = $2',
|
||||
['vehicle-123', 'user-123']
|
||||
);
|
||||
expect(repositoryInstance.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should generate unique IDs for documents', async () => {
|
||||
mockPool.query.mockResolvedValue({ rows: [{ id: 'vehicle-123' }] });
|
||||
repositoryInstance.insert.mockResolvedValue(mockCreatedDocument);
|
||||
|
||||
await service.createDocument('user-123', mockDocumentBody);
|
||||
await service.createDocument('user-123', mockDocumentBody);
|
||||
|
||||
expect(repositoryInstance.insert).toHaveBeenCalledTimes(2);
|
||||
const firstCall = repositoryInstance.insert.mock.calls[0][0];
|
||||
const secondCall = repositoryInstance.insert.mock.calls[1][0];
|
||||
expect(firstCall.id).not.toEqual(secondCall.id);
|
||||
expect(firstCall.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDocument', () => {
|
||||
it('should return document if found', async () => {
|
||||
const mockDocument = {
|
||||
id: 'doc-123',
|
||||
user_id: 'user-123',
|
||||
title: 'Test Document',
|
||||
};
|
||||
repositoryInstance.findById.mockResolvedValue(mockDocument as any);
|
||||
|
||||
const result = await service.getDocument('user-123', 'doc-123');
|
||||
|
||||
expect(repositoryInstance.findById).toHaveBeenCalledWith('doc-123', 'user-123');
|
||||
expect(result).toEqual(mockDocument);
|
||||
});
|
||||
|
||||
it('should return null if document not found', async () => {
|
||||
repositoryInstance.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getDocument('user-123', 'doc-123');
|
||||
|
||||
expect(repositoryInstance.findById).toHaveBeenCalledWith('doc-123', 'user-123');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listDocuments', () => {
|
||||
const mockDocuments = [
|
||||
{ id: 'doc-1', title: 'Insurance', document_type: 'insurance' },
|
||||
{ id: 'doc-2', title: 'Registration', document_type: 'registration' },
|
||||
];
|
||||
|
||||
it('should list all user documents without filters', async () => {
|
||||
repositoryInstance.listByUser.mockResolvedValue(mockDocuments as any);
|
||||
|
||||
const result = await service.listDocuments('user-123');
|
||||
|
||||
expect(repositoryInstance.listByUser).toHaveBeenCalledWith('user-123', undefined);
|
||||
expect(result).toEqual(mockDocuments);
|
||||
});
|
||||
|
||||
it('should list documents with filters', async () => {
|
||||
const filters = {
|
||||
vehicleId: 'vehicle-123',
|
||||
type: 'insurance' as const,
|
||||
expiresBefore: '2024-12-31',
|
||||
};
|
||||
repositoryInstance.listByUser.mockResolvedValue([mockDocuments[0]] as any);
|
||||
|
||||
const result = await service.listDocuments('user-123', filters);
|
||||
|
||||
expect(repositoryInstance.listByUser).toHaveBeenCalledWith('user-123', filters);
|
||||
expect(result).toEqual([mockDocuments[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDocument', () => {
|
||||
const mockExistingDocument = {
|
||||
id: 'doc-123',
|
||||
user_id: 'user-123',
|
||||
title: 'Original Title',
|
||||
};
|
||||
|
||||
it('should update document successfully', async () => {
|
||||
const updateData = { title: 'Updated Title', notes: 'Updated notes' };
|
||||
const updatedDocument = { ...mockExistingDocument, ...updateData };
|
||||
|
||||
repositoryInstance.findById.mockResolvedValue(mockExistingDocument as any);
|
||||
repositoryInstance.updateMetadata.mockResolvedValue(updatedDocument as any);
|
||||
|
||||
const result = await service.updateDocument('user-123', 'doc-123', updateData);
|
||||
|
||||
expect(repositoryInstance.findById).toHaveBeenCalledWith('doc-123', 'user-123');
|
||||
expect(repositoryInstance.updateMetadata).toHaveBeenCalledWith('doc-123', 'user-123', updateData);
|
||||
expect(result).toEqual(updatedDocument);
|
||||
});
|
||||
|
||||
it('should return null if document not found', async () => {
|
||||
repositoryInstance.findById.mockResolvedValue(null);
|
||||
|
||||
const result = await service.updateDocument('user-123', 'doc-123', { title: 'New Title' });
|
||||
|
||||
expect(repositoryInstance.findById).toHaveBeenCalledWith('doc-123', 'user-123');
|
||||
expect(repositoryInstance.updateMetadata).not.toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return existing document if no valid patch provided', async () => {
|
||||
repositoryInstance.findById.mockResolvedValue(mockExistingDocument as any);
|
||||
|
||||
const result = await service.updateDocument('user-123', 'doc-123', null as any);
|
||||
|
||||
expect(repositoryInstance.findById).toHaveBeenCalledWith('doc-123', 'user-123');
|
||||
expect(repositoryInstance.updateMetadata).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(mockExistingDocument);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDocument', () => {
|
||||
it('should delete document successfully', async () => {
|
||||
repositoryInstance.softDelete.mockResolvedValue(undefined);
|
||||
|
||||
await service.deleteDocument('user-123', 'doc-123');
|
||||
|
||||
expect(repositoryInstance.softDelete).toHaveBeenCalledWith('doc-123', 'user-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* @ai-summary Unit tests for MinIO storage adapter
|
||||
* @ai-context Tests storage layer with mocked MinIO client
|
||||
*/
|
||||
|
||||
import { createMinioAdapter } from '../../../../core/storage/adapters/minio.adapter';
|
||||
import { Client as MinioClient } from 'minio';
|
||||
import { appConfig } from '../../../../core/config/config-loader';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('minio');
|
||||
jest.mock('../../../../core/config/config-loader');
|
||||
|
||||
const mockMinioClient = jest.mocked(MinioClient);
|
||||
const mockAppConfig = jest.mocked(appConfig);
|
||||
|
||||
describe('MinIO Storage Adapter', () => {
|
||||
let clientInstance: jest.Mocked<MinioClient>;
|
||||
let adapter: ReturnType<typeof createMinioAdapter>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
clientInstance = {
|
||||
putObject: jest.fn(),
|
||||
getObject: jest.fn(),
|
||||
removeObject: jest.fn(),
|
||||
statObject: jest.fn(),
|
||||
presignedGetObject: jest.fn(),
|
||||
presignedPutObject: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockMinioClient.mockImplementation(() => clientInstance);
|
||||
|
||||
mockAppConfig.getMinioConfig.mockReturnValue({
|
||||
endpoint: 'localhost',
|
||||
port: 9000,
|
||||
accessKey: 'testkey',
|
||||
secretKey: 'testsecret',
|
||||
bucket: 'test-bucket',
|
||||
});
|
||||
|
||||
adapter = createMinioAdapter();
|
||||
});
|
||||
|
||||
describe('putObject', () => {
|
||||
it('should upload Buffer with correct parameters', async () => {
|
||||
const buffer = Buffer.from('test content');
|
||||
clientInstance.putObject.mockResolvedValue('etag-123');
|
||||
|
||||
await adapter.putObject('test-bucket', 'test-key', buffer, 'text/plain', { 'x-custom': 'value' });
|
||||
|
||||
expect(clientInstance.putObject).toHaveBeenCalledWith(
|
||||
'test-bucket',
|
||||
'test-key',
|
||||
buffer,
|
||||
buffer.length,
|
||||
{
|
||||
'Content-Type': 'text/plain',
|
||||
'x-custom': 'value',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should upload string with correct parameters', async () => {
|
||||
const content = 'test content';
|
||||
clientInstance.putObject.mockResolvedValue('etag-123');
|
||||
|
||||
await adapter.putObject('test-bucket', 'test-key', content, 'text/plain');
|
||||
|
||||
expect(clientInstance.putObject).toHaveBeenCalledWith(
|
||||
'test-bucket',
|
||||
'test-key',
|
||||
content,
|
||||
content.length,
|
||||
{ 'Content-Type': 'text/plain' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should upload stream without size', async () => {
|
||||
const stream = new Readable();
|
||||
clientInstance.putObject.mockResolvedValue('etag-123');
|
||||
|
||||
await adapter.putObject('test-bucket', 'test-key', stream, 'application/octet-stream');
|
||||
|
||||
expect(clientInstance.putObject).toHaveBeenCalledWith(
|
||||
'test-bucket',
|
||||
'test-key',
|
||||
stream,
|
||||
undefined,
|
||||
{ 'Content-Type': 'application/octet-stream' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle upload without content type', async () => {
|
||||
const buffer = Buffer.from('test');
|
||||
clientInstance.putObject.mockResolvedValue('etag-123');
|
||||
|
||||
await adapter.putObject('test-bucket', 'test-key', buffer);
|
||||
|
||||
expect(clientInstance.putObject).toHaveBeenCalledWith(
|
||||
'test-bucket',
|
||||
'test-key',
|
||||
buffer,
|
||||
buffer.length,
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getObjectStream', () => {
|
||||
it('should return object stream', async () => {
|
||||
const mockStream = new Readable();
|
||||
clientInstance.getObject.mockResolvedValue(mockStream);
|
||||
|
||||
const result = await adapter.getObjectStream('test-bucket', 'test-key');
|
||||
|
||||
expect(clientInstance.getObject).toHaveBeenCalledWith('test-bucket', 'test-key');
|
||||
expect(result).toBe(mockStream);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteObject', () => {
|
||||
it('should remove object', async () => {
|
||||
clientInstance.removeObject.mockResolvedValue(undefined);
|
||||
|
||||
await adapter.deleteObject('test-bucket', 'test-key');
|
||||
|
||||
expect(clientInstance.removeObject).toHaveBeenCalledWith('test-bucket', 'test-key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('headObject', () => {
|
||||
it('should return object metadata', async () => {
|
||||
const mockStat = {
|
||||
size: 1024,
|
||||
etag: 'test-etag',
|
||||
lastModified: '2024-01-01T00:00:00Z',
|
||||
metaData: {
|
||||
'content-type': 'application/pdf',
|
||||
'x-custom-header': 'custom-value',
|
||||
},
|
||||
};
|
||||
clientInstance.statObject.mockResolvedValue(mockStat);
|
||||
|
||||
const result = await adapter.headObject('test-bucket', 'test-key');
|
||||
|
||||
expect(clientInstance.statObject).toHaveBeenCalledWith('test-bucket', 'test-key');
|
||||
expect(result).toEqual({
|
||||
size: 1024,
|
||||
etag: 'test-etag',
|
||||
lastModified: new Date('2024-01-01T00:00:00Z'),
|
||||
contentType: 'application/pdf',
|
||||
metadata: mockStat.metaData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle metadata with Content-Type header', async () => {
|
||||
const mockStat = {
|
||||
size: 1024,
|
||||
etag: 'test-etag',
|
||||
lastModified: '2024-01-01T00:00:00Z',
|
||||
metaData: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
},
|
||||
};
|
||||
clientInstance.statObject.mockResolvedValue(mockStat);
|
||||
|
||||
const result = await adapter.headObject('test-bucket', 'test-key');
|
||||
|
||||
expect(result.contentType).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle missing optional fields', async () => {
|
||||
const mockStat = {
|
||||
size: 1024,
|
||||
etag: 'test-etag',
|
||||
};
|
||||
clientInstance.statObject.mockResolvedValue(mockStat);
|
||||
|
||||
const result = await adapter.headObject('test-bucket', 'test-key');
|
||||
|
||||
expect(result).toEqual({
|
||||
size: 1024,
|
||||
etag: 'test-etag',
|
||||
lastModified: undefined,
|
||||
contentType: undefined,
|
||||
metadata: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSignedUrl', () => {
|
||||
it('should generate GET signed URL with default expiry', async () => {
|
||||
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
|
||||
|
||||
const result = await adapter.getSignedUrl('test-bucket', 'test-key');
|
||||
|
||||
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 300);
|
||||
expect(result).toBe('https://example.com/signed-url');
|
||||
});
|
||||
|
||||
it('should generate GET signed URL with custom expiry', async () => {
|
||||
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
|
||||
|
||||
const result = await adapter.getSignedUrl('test-bucket', 'test-key', {
|
||||
method: 'GET',
|
||||
expiresSeconds: 600,
|
||||
});
|
||||
|
||||
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 600);
|
||||
expect(result).toBe('https://example.com/signed-url');
|
||||
});
|
||||
|
||||
it('should generate PUT signed URL', async () => {
|
||||
clientInstance.presignedPutObject.mockResolvedValue('https://example.com/put-url');
|
||||
|
||||
const result = await adapter.getSignedUrl('test-bucket', 'test-key', {
|
||||
method: 'PUT',
|
||||
expiresSeconds: 300,
|
||||
});
|
||||
|
||||
expect(clientInstance.presignedPutObject).toHaveBeenCalledWith('test-bucket', 'test-key', 300);
|
||||
expect(result).toBe('https://example.com/put-url');
|
||||
});
|
||||
|
||||
it('should enforce minimum expiry time', async () => {
|
||||
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
|
||||
|
||||
await adapter.getSignedUrl('test-bucket', 'test-key', { expiresSeconds: 0 });
|
||||
|
||||
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 1);
|
||||
});
|
||||
|
||||
it('should enforce maximum expiry time', async () => {
|
||||
clientInstance.presignedGetObject.mockResolvedValue('https://example.com/signed-url');
|
||||
|
||||
await adapter.getSignedUrl('test-bucket', 'test-key', { expiresSeconds: 10000000 });
|
||||
|
||||
expect(clientInstance.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'test-key', 604800); // 7 days max
|
||||
});
|
||||
});
|
||||
|
||||
describe('MinioClient instantiation', () => {
|
||||
it('should create client with correct configuration', () => {
|
||||
expect(mockMinioClient).toHaveBeenCalledWith({
|
||||
endPoint: 'localhost',
|
||||
port: 9000,
|
||||
useSSL: false,
|
||||
accessKey: 'testkey',
|
||||
secretKey: 'testsecret',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user