Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
166 lines
5.5 KiB
TypeScript
166 lines
5.5 KiB
TypeScript
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' }),
|
|
})
|
|
);
|
|
});
|
|
});
|