app/api/files/specs/uploadRoutes.spec.ts
import path from 'path';
import request, { Response as SuperTestResponse } from 'supertest';
import { Application, Request, Response, NextFunction } from 'express';
import { search } from 'api/search';
import { uploadsPath, customUploadsPath, storage } from 'api/files';
import { setUpApp, socketEmit, iosocket, TestEmitSources } from 'api/utils/testingRoutes';
import { FileType } from 'shared/types/fileType';
import entities from 'api/entities';
import { testingEnvironment } from 'api/utils/testingEnvironment';
// eslint-disable-next-line node/no-restricted-import
import fs from 'fs/promises';
import { fixtures, templateId, importTemplate, adminUser, collabUser } from './fixtures';
import { files } from '../files';
import uploadRoutes from '../routes';
import { UserSchema } from 'shared/types/userType';
jest.mock(
'../../auth/authMiddleware.ts',
() => () => (_req: Request, _res: Response, next: NextFunction) => {
next();
}
);
describe('upload routes', () => {
let requestMockedUser: UserSchema = collabUser;
const app: Application = setUpApp(
uploadRoutes,
(req: Request, _res: Response, next: NextFunction) => {
(req as any).user = (() => requestMockedUser)();
next();
}
);
const mockCurrentUser = (user: UserSchema) => {
requestMockedUser = user;
testingEnvironment.setPermissions(user);
};
beforeEach(async () => {
jest.spyOn(search, 'indexEntities').mockImplementation(async () => Promise.resolve());
jest.spyOn(Date, 'now').mockReturnValue(1000);
await testingEnvironment.setUp(fixtures);
});
afterAll(async () => testingEnvironment.tearDown());
const uploadDocument = async (filepath: string): Promise<SuperTestResponse> =>
socketEmit('documentProcessed', async () =>
request(app)
.post('/api/files/upload/document')
.field('entity', 'sharedId1')
.attach('file', path.join(__dirname, filepath))
);
describe('POST/files/upload/documents', () => {
it('should upload the file', async () => {
await uploadDocument('uploads/f2082bf51b6ef839690485d7153e847a.pdf');
const [upload] = await files.get({ entity: 'sharedId1' }, '+fullText');
expect(await storage.fileExists(upload.filename!, 'document')).toBe(true);
}, 10000);
it('should process and reindex the document after upload', async () => {
const res: SuperTestResponse = await uploadDocument(
'uploads/f2082bf51b6ef839690485d7153e847a.pdf'
);
expect(res.body).toEqual(
expect.objectContaining({
originalname: 'f2082bf51b6ef839690485d7153e847a.pdf',
status: 'ready',
})
);
expect(iosocket.emit).toHaveBeenCalledWith(
'conversionStart',
TestEmitSources.session,
'sharedId1'
);
expect(iosocket.emit).toHaveBeenCalledWith(
'documentProcessed',
TestEmitSources.session,
'sharedId1'
);
const [upload] = await files.get(
{ originalname: 'f2082bf51b6ef839690485d7153e847a.pdf' },
'+fullText'
);
expect(upload).toEqual(
expect.objectContaining({
entity: 'sharedId1',
type: 'document',
status: 'ready',
fullText: { 1: 'Test[[1]] file[[1]]\n\n' },
totalPages: 1,
language: 'other',
filename: expect.stringMatching(/.*\.pdf/),
originalname: 'f2082bf51b6ef839690485d7153e847a.pdf',
creationDate: 1000,
})
);
}, 10000);
it('should generate a thumbnail for the document', async () => {
await uploadDocument('uploads/f2082bf51b6ef839690485d7153e847a.pdf');
const [{ filename = '', language, mimetype }] = await files.get({
entity: 'sharedId1',
type: 'thumbnail',
});
expect(language).toBe('other');
expect(mimetype).toEqual('image/jpeg');
expect(await fs.readFile(uploadsPath(filename))).toBeDefined();
});
describe('Language detection', () => {
it('should detect English documents and store the result', async () => {
await uploadDocument('uploads/eng.pdf');
const [upload] = await files.get({ originalname: 'eng.pdf' });
expect(upload.language).toBe('eng');
}, 10000);
it('should detect Spanish documents and store the result', async () => {
await uploadDocument('uploads/spn.pdf');
const [upload] = await files.get({ originalname: 'spn.pdf' });
expect(upload.language).toBe('spa');
});
});
describe('when conversion fails', () => {
it('should set document status to failed and emit a socket conversionFailed event with the id of the document', async () => {
await socketEmit('conversionFailed', async () =>
request(app)
.post('/api/files/upload/document')
.field('entity', 'sharedId1')
.attach('file', path.join(__dirname, 'uploads/invalid_document.txt'))
);
const [upload] = await files.get({ originalname: 'invalid_document.txt' }, '+fullText');
expect(upload.status).toBe('failed');
});
it('should return the file object', async () => {
const response: SuperTestResponse = await request(app)
.post('/api/files/upload/document')
.field('entity', 'sharedId1')
.attach('file', path.join(__dirname, 'uploads/invalid_document.txt'));
expect(response.body.status).toBe('failed');
expect(response.body._id).toBeDefined();
expect(response.body.originalname).toBe('invalid_document.txt');
});
});
});
describe('POST/files/upload/custom', () => {
it('should save the upload and return it', async () => {
const response: SuperTestResponse = await request(app)
.post('/api/files/upload/custom')
.attach('file', path.join(__dirname, 'test.txt'));
expect(response.body).toEqual(
expect.objectContaining({
type: 'custom',
filename: expect.stringMatching(/.*\.txt/),
mimetype: 'text/plain',
originalname: 'test.txt',
size: 5,
})
);
});
it('should save the file on customUploads path', async () => {
await request(app)
.post('/api/files/upload/custom')
.attach('file', path.join(__dirname, 'test.txt'));
const [file]: FileType[] = await files.get({ originalname: 'test.txt' });
expect(await fs.readFile(customUploadsPath(file.filename || ''))).toBeDefined();
});
});
describe('POST /api/import', () => {
it('should import the entried from the csv file', async () => {
await socketEmit('IMPORT_CSV_END', async () =>
request(app)
.post('/api/import')
.field('template', importTemplate.toString())
.attach('file', `${__dirname}/uploads/importcsv.csv`)
);
expect(iosocket.emit).toHaveBeenCalledWith('IMPORT_CSV_START', TestEmitSources.session);
expect(iosocket.emit).toHaveBeenCalledWith('IMPORT_CSV_PROGRESS', TestEmitSources.session, 1);
expect(iosocket.emit).toHaveBeenCalledWith('IMPORT_CSV_PROGRESS', TestEmitSources.session, 2);
const imported = await entities.get({ template: importTemplate });
expect(imported).toEqual([
expect.objectContaining({ title: 'imported entity one' }),
expect.objectContaining({ title: 'imported entity two' }),
]);
});
describe('on error', () => {
it('should emit the error', async () => {
await socketEmit('IMPORT_CSV_ERROR', async () =>
request(app)
.post('/api/import')
.field('template', templateId.toString())
.attach('file', `${__dirname}/uploads/import.zip`)
);
expect(iosocket.emit).toHaveBeenCalledWith(
'IMPORT_CSV_ERROR',
TestEmitSources.session,
expect.objectContaining({ code: 500 })
);
});
});
});
describe('DELETE/files', () => {
it('should delete thumbnails asociated with documents deleted', async () => {
mockCurrentUser(adminUser);
await uploadDocument('uploads/f2082bf51b6ef839690485d7153e847a.pdf');
const [file]: FileType[] = await files.get({
originalname: 'f2082bf51b6ef839690485d7153e847a.pdf',
});
await request(app).delete('/api/files').query({ _id: file._id?.toString() });
const [thumbnail]: FileType[] = await files.get({ filename: `${file._id}.jpg` });
expect(thumbnail).not.toBeDefined();
});
});
});