import { describe, expect, it, vi, beforeEach } from 'vitest'; vi.mock('@alga-psa/db', () => ({ createTenantKnex: vi.fn(), })); vi.mock('@alga-psa/event-bus/publishers', () => ({ publishWorkflowEvent: vi.fn(), })); vi.mock('../src/config/storage', () => ({ getProviderConfig: vi.fn(), getStorageConfig: vi.fn(), validateFileUpload: vi.fn(async () => {}), })); vi.mock('../src/StorageProviderFactory', () => ({ StorageProviderFactory: { createProvider: vi.fn(), }, generateStoragePath: vi.fn(() => 'tenant-1/files/sample.txt'), })); vi.mock('../src/models/storage', () => ({ FileStoreModel: { create: vi.fn(), findById: vi.fn(), softDelete: vi.fn(), }, })); vi.mock('@alga-psa/validation', () => ({ isValidUUID: vi.fn(() => true), })); vi.mock('file-type', () => ({ fileTypeFromBuffer: vi.fn(async () => undefined), })); import { createTenantKnex } from '@alga-psa/db'; import { publishWorkflowEvent } from '@alga-psa/event-bus/publishers'; import { StorageProviderFactory } from '../src/StorageProviderFactory'; import { getProviderConfig, getStorageConfig } from '../src/config/storage'; import { FileStoreModel } from '../src/models/storage'; import { StorageService } from '../src/StorageService'; describe('StorageService.uploadFile workflow events', () => { const createTenantKnexMock = vi.mocked(createTenantKnex); const publishWorkflowEventMock = vi.mocked(publishWorkflowEvent); const createProviderMock = vi.mocked(StorageProviderFactory.createProvider); const getStorageConfigMock = vi.mocked(getStorageConfig); const getProviderConfigMock = vi.mocked(getProviderConfig); const fileCreateMock = vi.mocked(FileStoreModel.create); const fileFindByIdMock = vi.mocked(FileStoreModel.findById); const fileSoftDeleteMock = vi.mocked(FileStoreModel.softDelete); beforeEach(() => { publishWorkflowEventMock.mockReset(); createProviderMock.mockReset(); getStorageConfigMock.mockReset(); getProviderConfigMock.mockReset(); fileCreateMock.mockReset(); fileFindByIdMock.mockReset(); fileSoftDeleteMock.mockReset(); createTenantKnexMock.mockReset(); }); it('publishes FILE_UPLOADED after creating the file record', async () => { createProviderMock.mockResolvedValue({ upload: vi.fn(async () => ({ path: 'tenant-1/files/sample.txt' })), } as any); createTenantKnexMock.mockResolvedValue({ knex: {} } as any); fileCreateMock.mockResolvedValue({ file_id: '14f1fbf4-17d6-4bdc-8d4b-0b2a2ff8f26a', original_name: 'sample.txt', mime_type: 'text/plain', file_size: 3, storage_path: 'tenant-1/files/sample.txt', uploaded_by_id: 'a836a8b5-3df5-47b1-b49b-9a78f2b1a8a0', created_at: '2026-01-24T12:00:00.000Z', } as any); await StorageService.uploadFile('tenant-1', Buffer.from('abc'), 'sample.txt', { mime_type: 'text/plain', uploaded_by_id: 'a836a8b5-3df5-47b1-b49b-9a78f2b1a8a0', }); expect(publishWorkflowEventMock).toHaveBeenCalledWith( expect.objectContaining({ eventType: 'DOCUMENT_UPLOADED', payload: expect.objectContaining({ documentId: '14f1fbf4-17d6-4bdc-8d4b-0b2a2ff8f26a', uploadedByUserId: 'a836a8b5-3df5-47b1-b49b-9a78f2b1a8a0', uploadedAt: '2026-01-24T12:00:00.000Z', fileName: 'sample.txt', contentType: 'text/plain', sizeBytes: 3, storageKey: 'tenant-1/files/sample.txt', }), ctx: expect.objectContaining({ tenantId: 'tenant-1' }), }) ); expect(publishWorkflowEventMock).toHaveBeenCalledWith( expect.objectContaining({ eventType: 'FILE_UPLOADED', payload: expect.objectContaining({ fileId: '14f1fbf4-17d6-4bdc-8d4b-0b2a2ff8f26a', fileName: 'sample.txt', contentType: 'text/plain', sizeBytes: 3, storageKey: 'tenant-1/files/sample.txt', }), ctx: expect.objectContaining({ tenantId: 'tenant-1' }), }) ); }); it('publishes DOCUMENT_DELETED after deleting the file record', async () => { getStorageConfigMock.mockResolvedValue({ defaultProvider: 'local' } as any); getProviderConfigMock.mockResolvedValue({ type: 'local' } as any); createProviderMock.mockResolvedValue({ delete: vi.fn(async () => {}), } as any); createTenantKnexMock.mockResolvedValue({ knex: {}, tenant: 'tenant-1' } as any); fileFindByIdMock .mockResolvedValueOnce({ file_id: '14f1fbf4-17d6-4bdc-8d4b-0b2a2ff8f26a', original_name: 'sample.txt', mime_type: 'text/plain', file_size: 3, storage_path: 'tenant-1/files/sample.txt', uploaded_by_id: 'a836a8b5-3df5-47b1-b49b-9a78f2b1a8a0', created_at: '2026-01-24T12:00:00.000Z', } as any) .mockResolvedValueOnce({ file_id: '14f1fbf4-17d6-4bdc-8d4b-0b2a2ff8f26a', deleted_at: '2026-01-24T12:30:00.000Z', } as any); fileSoftDeleteMock.mockResolvedValue(undefined as any); await StorageService.deleteFile( '14f1fbf4-17d6-4bdc-8d4b-0b2a2ff8f26a', 'a836a8b5-3df5-47b1-b49b-9a78f2b1a8a0' ); expect(publishWorkflowEventMock).toHaveBeenCalledWith( expect.objectContaining({ eventType: 'DOCUMENT_DELETED', payload: expect.objectContaining({ documentId: '14f1fbf4-17d6-4bdc-8d4b-0b2a2ff8f26a', deletedByUserId: 'a836a8b5-3df5-47b1-b49b-9a78f2b1a8a0', deletedAt: '2026-01-24T12:30:00.000Z', }), ctx: expect.objectContaining({ tenantId: 'tenant-1' }), }) ); }); });