mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Server: Moved file API tests to route
This commit is contained in:
parent
ecb6134828
commit
66a09e5068
@ -1,535 +0,0 @@
|
||||
import { testAssetDir, createUserAndSession, createUser, checkThrowAsync, beforeAllDb, afterAllDb, beforeEachDb, models, controllers } from '../../utils/testing/testUtils';
|
||||
import * as fs from 'fs-extra';
|
||||
import { ChangeType, File } from '../../db';
|
||||
import { ErrorConflict, ErrorForbidden, ErrorNotFound, ErrorUnprocessableEntity } from '../../utils/errors';
|
||||
import { filePathInfo } from '../../utils/routeUtils';
|
||||
import { defaultPagination, Pagination, PaginationOrderDir } from '../../models/utils/pagination';
|
||||
import { msleep } from '../../utils/time';
|
||||
|
||||
async function makeTestFile(id: number = 1, ext: string = 'jpg', parentId: string = ''): Promise<File> {
|
||||
const basename = ext === 'jpg' ? 'photo' : 'poster';
|
||||
|
||||
const file: File = {
|
||||
name: id > 1 ? `${basename}-${id}.${ext}` : `${basename}.${ext}`,
|
||||
content: await fs.readFile(`${testAssetDir}/${basename}.${ext}`),
|
||||
// mime_type: `image/${ext}`,
|
||||
parent_id: parentId,
|
||||
};
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
async function makeTestContent(ext: string = 'jpg') {
|
||||
const basename = ext === 'jpg' ? 'photo' : 'poster';
|
||||
return await fs.readFile(`${testAssetDir}/${basename}.${ext}`);
|
||||
}
|
||||
|
||||
async function makeTestDirectory(name: string = 'Docs'): Promise<File> {
|
||||
const file: File = {
|
||||
name: name,
|
||||
parent_id: '',
|
||||
is_directory: 1,
|
||||
};
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
async function saveTestFile(sessionId: string, path: string): Promise<File> {
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
return fileController.putFileContent(
|
||||
sessionId,
|
||||
path,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function saveTestDir(sessionId: string, path: string): Promise<File> {
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
const parsed = filePathInfo(path);
|
||||
|
||||
return fileController.postChild(
|
||||
sessionId,
|
||||
parsed.dirname,
|
||||
{
|
||||
name: parsed.basename,
|
||||
is_directory: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
describe('FileController', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('FileController');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should create a file', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
const fileContent = await makeTestContent();
|
||||
|
||||
const newFile = await fileController.putFileContent(
|
||||
session.id,
|
||||
'root:/photo.jpg:',
|
||||
fileContent
|
||||
);
|
||||
|
||||
expect(!!newFile.id).toBe(true);
|
||||
expect(newFile.name).toBe('photo.jpg');
|
||||
expect(newFile.mime_type).toBe('image/jpeg');
|
||||
expect(!!newFile.parent_id).toBe(true);
|
||||
expect(!newFile.content).toBe(true);
|
||||
expect(newFile.size > 0).toBe(true);
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
const newFileReload = await fileModel.loadWithContent(newFile.id);
|
||||
|
||||
expect(!!newFileReload).toBe(true);
|
||||
|
||||
const newFileHex = fileContent.toString('hex');
|
||||
const newFileReloadHex = (newFileReload.content as Buffer).toString('hex');
|
||||
expect(newFileReloadHex.length > 0).toBe(true);
|
||||
expect(newFileReloadHex).toBe(newFileHex);
|
||||
});
|
||||
|
||||
test('should create sub-directories', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
const newDir = await fileController.postFile_(session.id, {
|
||||
is_directory: 1,
|
||||
name: 'subdir',
|
||||
});
|
||||
|
||||
expect(!!newDir.id).toBe(true);
|
||||
expect(newDir.is_directory).toBe(1);
|
||||
|
||||
const newDir2 = await fileController.postFile_(session.id, {
|
||||
is_directory: 1,
|
||||
name: 'subdir2',
|
||||
parent_id: newDir.id,
|
||||
});
|
||||
|
||||
const newDirReload2 = await fileController.getFile(session.id, 'root:/subdir/subdir2');
|
||||
expect(newDirReload2.id).toBe(newDir2.id);
|
||||
expect(newDirReload2.name).toBe(newDir2.name);
|
||||
});
|
||||
|
||||
test('should create files in sub-directory', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
await fileController.postFile_(session.id, {
|
||||
is_directory: 1,
|
||||
name: 'subdir',
|
||||
});
|
||||
|
||||
const newFile = await fileController.putFileContent(
|
||||
session.id,
|
||||
'root:/subdir/photo.jpg:',
|
||||
await makeTestContent()
|
||||
);
|
||||
|
||||
const newFileReload = await fileController.getFile(session.id, 'root:/subdir/photo.jpg');
|
||||
expect(newFileReload.id).toBe(newFile.id);
|
||||
expect(newFileReload.name).toBe('photo.jpg');
|
||||
});
|
||||
|
||||
test('should not create a file with an invalid path', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
const fileContent = await makeTestContent();
|
||||
|
||||
const error = await checkThrowAsync(async () => fileController.putFileContent(
|
||||
session.id,
|
||||
'root:/does/not/exist/photo.jpg:',
|
||||
fileContent
|
||||
));
|
||||
|
||||
expect(error instanceof ErrorNotFound).toBe(true);
|
||||
});
|
||||
|
||||
test('should get files', async function() {
|
||||
const { session: session1, user: user1 } = await createUserAndSession(1);
|
||||
const { session: session2 } = await createUserAndSession(2);
|
||||
|
||||
let file1: File = await makeTestFile(1);
|
||||
let file2: File = await makeTestFile(2);
|
||||
let file3: File = await makeTestFile(3);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
file1 = await fileController.postFile_(session1.id, file1);
|
||||
file2 = await fileController.postFile_(session1.id, file2);
|
||||
file3 = await fileController.postFile_(session2.id, file3);
|
||||
|
||||
const fileId1 = file1.id;
|
||||
const fileId2 = file2.id;
|
||||
|
||||
// Can't get someone else file
|
||||
const error = await checkThrowAsync(async () => fileController.getFile(session1.id, file3.id));
|
||||
expect(error instanceof ErrorForbidden).toBe(true);
|
||||
|
||||
file1 = await fileController.getFile(session1.id, file1.id);
|
||||
expect(file1.id).toBe(fileId1);
|
||||
|
||||
const fileModel = models().file({ userId: user1.id });
|
||||
const paginatedResults = await fileController.getChildren(session1.id, await fileModel.userRootFileId(), defaultPagination());
|
||||
const allFiles = paginatedResults.items;
|
||||
expect(allFiles.length).toBe(2);
|
||||
expect(JSON.stringify(allFiles.map(f => f.id).sort())).toBe(JSON.stringify([fileId1, fileId2].sort()));
|
||||
});
|
||||
|
||||
test('should not let create a file in a directory not owned by user', async function() {
|
||||
const { session } = await createUserAndSession(1);
|
||||
|
||||
const user2 = await createUser(2);
|
||||
const fileModel2 = models().file({ userId: user2.id });
|
||||
const rootFile2 = await fileModel2.userRootFile();
|
||||
|
||||
const file: File = await makeTestFile();
|
||||
file.parent_id = rootFile2.id;
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
const hasThrown = await checkThrowAsync(async () => fileController.postFile_(session.id, file));
|
||||
expect(!!hasThrown).toBe(true);
|
||||
});
|
||||
|
||||
test('should update file properties', async function() {
|
||||
const { session, user } = await createUserAndSession(1, true);
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
let file: File = await makeTestFile();
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
file = await fileController.postFile_(session.id, file);
|
||||
|
||||
// Can't have file with empty name
|
||||
const error = await checkThrowAsync(async () => fileController.patchFile(session.id, file.id, { name: '' }));
|
||||
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
|
||||
|
||||
await fileController.patchFile(session.id, file.id, { name: 'modified.jpg' });
|
||||
file = await fileModel.load(file.id);
|
||||
expect(file.name).toBe('modified.jpg');
|
||||
|
||||
await fileController.patchFile(session.id, file.id, { mime_type: 'image/png' });
|
||||
file = await fileModel.load(file.id);
|
||||
expect(file.mime_type).toBe('image/png');
|
||||
});
|
||||
|
||||
test('should not allow duplicate filenames', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
let file1: File = await makeTestFile(1);
|
||||
const file2: File = await makeTestFile(1);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
file1 = await fileController.postFile_(session.id, file1);
|
||||
|
||||
expect(!!file1.id).toBe(true);
|
||||
expect(file1.name).toBe(file2.name);
|
||||
|
||||
const hasThrown = await checkThrowAsync(async () => await fileController.postFile_(session.id, file2));
|
||||
expect(!!hasThrown).toBe(true);
|
||||
});
|
||||
|
||||
test('should change the file parent', async function() {
|
||||
const { session: session1, user: user1 } = await createUserAndSession(1);
|
||||
const { user: user2 } = await createUserAndSession(2);
|
||||
let hasThrown: any = null;
|
||||
|
||||
const fileModel = models().file({ userId: user1.id });
|
||||
|
||||
let file: File = await makeTestFile();
|
||||
let file2: File = await makeTestFile(2);
|
||||
let dir: File = await makeTestDirectory();
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
file = await fileController.postFile_(session1.id, file);
|
||||
file2 = await fileController.postFile_(session1.id, file2);
|
||||
dir = await fileController.postFile_(session1.id, dir);
|
||||
|
||||
// Can't set parent to another non-directory file
|
||||
hasThrown = await checkThrowAsync(async () => await fileController.patchFile(session1.id, file.id, { parent_id: file2.id }));
|
||||
expect(!!hasThrown).toBe(true);
|
||||
|
||||
const fileModel2 = models().file({ userId: user2.id });
|
||||
const userRoot2 = await fileModel2.userRootFile();
|
||||
|
||||
// Can't set parent to someone else directory
|
||||
hasThrown = await checkThrowAsync(async () => await fileController.patchFile(session1.id, file.id, { parent_id: userRoot2.id }));
|
||||
expect(!!hasThrown).toBe(true);
|
||||
|
||||
await fileController.patchFile(session1.id, file.id, { parent_id: dir.id });
|
||||
|
||||
file = await fileModel.load(file.id);
|
||||
|
||||
expect(!!file.parent_id).toBe(true);
|
||||
expect(file.parent_id).toBe(dir.id);
|
||||
});
|
||||
|
||||
test('should delete a file', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
const file1: File = await makeTestFile(1);
|
||||
let file2: File = await makeTestFile(2);
|
||||
|
||||
await fileController.postFile_(session.id, file1);
|
||||
file2 = await fileController.postFile_(session.id, file2);
|
||||
let allFiles: File[] = await fileModel.all();
|
||||
const beforeCount: number = allFiles.length;
|
||||
|
||||
await fileController.deleteFile(session.id, file2.id);
|
||||
allFiles = await fileModel.all();
|
||||
expect(allFiles.length).toBe(beforeCount - 1);
|
||||
});
|
||||
|
||||
test('should create and delete directories', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
const dir1: File = await fileController.postChild(session.id, 'root', { name: 'dir1', is_directory: 1 });
|
||||
const dir2: File = await fileController.postChild(session.id, 'root:/dir1', { name: 'dir2', is_directory: 1 });
|
||||
|
||||
const dirReload2: File = await fileController.getFile(session.id, 'root:/dir1/dir2');
|
||||
expect(dirReload2.id).toBe(dir2.id);
|
||||
|
||||
// Delete one directory
|
||||
await fileController.deleteFile(session.id, 'root:/dir1/dir2');
|
||||
const error = await checkThrowAsync(async () => fileController.getFile(session.id, 'root:/dir1/dir2'));
|
||||
expect(error instanceof ErrorNotFound).toBe(true);
|
||||
|
||||
// Delete a directory and its sub-directories and files
|
||||
const dir3: File = await fileController.postChild(session.id, 'root:/dir1', { name: 'dir3', is_directory: 1 });
|
||||
const file1: File = await fileController.postFile_(session.id, { name: 'file1', parent_id: dir1.id });
|
||||
const file2: File = await fileController.postFile_(session.id, { name: 'file2', parent_id: dir3.id });
|
||||
await fileController.deleteFile(session.id, 'root:/dir1');
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
expect(!(await fileModel.load(dir1.id))).toBe(true);
|
||||
expect(!(await fileModel.load(dir3.id))).toBe(true);
|
||||
expect(!(await fileModel.load(file1.id))).toBe(true);
|
||||
expect(!(await fileModel.load(file2.id))).toBe(true);
|
||||
});
|
||||
|
||||
test('should not change the parent when updating a file', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
const dir1: File = await fileController.postChild(session.id, 'root', { name: 'dir1', is_directory: 1 });
|
||||
const file1: File = await fileController.putFileContent(session.id, 'root:/dir1/myfile.md', Buffer.from('testing'));
|
||||
|
||||
await fileController.putFileContent(session.id, 'root:/dir1/myfile.md', Buffer.from('new content'));
|
||||
const fileReloaded1 = await fileModel.load(file1.id);
|
||||
|
||||
expect(fileReloaded1.parent_id).toBe(dir1.id);
|
||||
});
|
||||
|
||||
test('should not delete someone else file', async function() {
|
||||
const { session: session1 } = await createUserAndSession(1);
|
||||
const { session: session2 } = await createUserAndSession(2);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
const file1: File = await makeTestFile(1);
|
||||
let file2: File = await makeTestFile(2);
|
||||
|
||||
await fileController.postFile_(session1.id, file1);
|
||||
file2 = await fileController.postFile_(session2.id, file2);
|
||||
|
||||
const error = await checkThrowAsync(async () => await fileController.deleteFile(session1.id, file2.id));
|
||||
expect(error instanceof ErrorForbidden).toBe(true);
|
||||
});
|
||||
|
||||
test('should let admin change or delete files', async function() {
|
||||
const { session: adminSession } = await createUserAndSession(1, true);
|
||||
const { session, user } = await createUserAndSession(2);
|
||||
|
||||
let file: File = await makeTestFile();
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
const fileController = controllers().apiFile();
|
||||
file = await fileController.postFile_(session.id, file);
|
||||
|
||||
await fileController.patchFile(adminSession.id, file.id, { name: 'modified.jpg' });
|
||||
file = await fileModel.load(file.id);
|
||||
expect(file.name).toBe('modified.jpg');
|
||||
|
||||
await fileController.deleteFile(adminSession.id, file.id);
|
||||
expect(!(await fileModel.load(file.id))).toBe(true);
|
||||
});
|
||||
|
||||
test('should update a file content', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const file: File = await makeTestFile(1);
|
||||
const file2: File = await makeTestFile(2, 'png');
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
const newFile = await fileController.postFile_(session.id, file);
|
||||
await fileController.putFileContent(session.id, newFile.id, file2.content);
|
||||
|
||||
const modFile = await fileController.getFileContent(session.id, newFile.id);
|
||||
|
||||
const originalFileHex = (file.content as Buffer).toString('hex');
|
||||
const modFileHex = (modFile.content as Buffer).toString('hex');
|
||||
expect(modFileHex.length > 0).toBe(true);
|
||||
expect(modFileHex === originalFileHex).toBe(false);
|
||||
expect(modFile.size).toBe(modFile.content.byteLength);
|
||||
expect(newFile.size).toBe(file.content.byteLength);
|
||||
});
|
||||
|
||||
test('should delete a file content', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const file: File = await makeTestFile(1);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
const newFile = await fileController.postFile_(session.id, file);
|
||||
await fileController.putFileContent(session.id, newFile.id, file.content);
|
||||
|
||||
await fileController.deleteFileContent(session.id, newFile.id);
|
||||
|
||||
const modFile = await fileController.getFile(session.id, newFile.id);
|
||||
expect(modFile.size).toBe(0);
|
||||
});
|
||||
|
||||
test('should not allow reserved characters', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const filenames = [
|
||||
'invalid*invalid',
|
||||
'invalid#invalid',
|
||||
'invalid\\invalid',
|
||||
];
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
for (const filename of filenames) {
|
||||
const error = await checkThrowAsync(async () => fileController.putFileContent(session.id, `root:/${filename}`, null));
|
||||
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not allow a directory with the same name', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
await saveTestDir(session.id, 'root:/somedir:');
|
||||
let error = await checkThrowAsync(async () => saveTestFile(session.id, 'root:/somedir:'));
|
||||
expect(error instanceof ErrorUnprocessableEntity).toBe(true);
|
||||
|
||||
await saveTestFile(session.id, 'root:/somefile.md:');
|
||||
error = await checkThrowAsync(async () => saveTestDir(session.id, 'root:/somefile.md:'));
|
||||
expect(error instanceof ErrorConflict).toBe(true);
|
||||
});
|
||||
|
||||
test('should not be possible to delete the root directory', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
const fileController = controllers().apiFile();
|
||||
|
||||
const error = await checkThrowAsync(async () => fileController.deleteFile(session.id, 'root'));
|
||||
expect(error instanceof ErrorForbidden).toBe(true);
|
||||
});
|
||||
|
||||
test('should support root:/: format, which means root', async function() {
|
||||
const { session, user } = await createUserAndSession(1, true);
|
||||
const fileController = controllers().apiFile();
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
const root = await fileController.getFile(session.id, 'root:/:');
|
||||
expect(root.id).toBe(await fileModel.userRootFileId());
|
||||
});
|
||||
|
||||
test('should paginate results', async function() {
|
||||
const { session: session1, user: user1 } = await createUserAndSession(1);
|
||||
|
||||
let file1: File = await makeTestFile(1);
|
||||
let file2: File = await makeTestFile(2);
|
||||
let file3: File = await makeTestFile(3);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
file1 = await fileController.postFile_(session1.id, file1);
|
||||
await msleep(1);
|
||||
file2 = await fileController.postFile_(session1.id, file2);
|
||||
await msleep(1);
|
||||
file3 = await fileController.postFile_(session1.id, file3);
|
||||
|
||||
const fileModel = models().file({ userId: user1.id });
|
||||
const rootId = await fileModel.userRootFileId();
|
||||
|
||||
const pagination: Pagination = {
|
||||
limit: 2,
|
||||
order: [
|
||||
{
|
||||
by: 'updated_time',
|
||||
dir: PaginationOrderDir.ASC,
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
};
|
||||
|
||||
for (const method of ['page', 'cursor']) {
|
||||
const page1 = await fileController.getChildren(session1.id, rootId, pagination);
|
||||
expect(page1.items.length).toBe(2);
|
||||
expect(page1.has_more).toBe(true);
|
||||
expect(page1.items[0].id).toBe(file1.id);
|
||||
expect(page1.items[1].id).toBe(file2.id);
|
||||
|
||||
const p = method === 'page' ? { ...pagination, page: 2 } : { cursor: page1.cursor };
|
||||
const page2 = await fileController.getChildren(session1.id, rootId, p);
|
||||
expect(page2.items.length).toBe(1);
|
||||
expect(page2.has_more).toBe(false);
|
||||
expect(page2.items[0].id).toBe(file3.id);
|
||||
}
|
||||
});
|
||||
|
||||
test('should track file changes', async function() {
|
||||
// We only do a basic check because most of the tests for this are in
|
||||
// ChangeModel.test.ts
|
||||
|
||||
const { session: session1 } = await createUserAndSession(1);
|
||||
|
||||
let file1: File = await makeTestFile(1);
|
||||
let file2: File = await makeTestFile(2);
|
||||
|
||||
const fileController = controllers().apiFile();
|
||||
file1 = await fileController.postFile_(session1.id, file1);
|
||||
await msleep(1); file2 = await fileController.postFile_(session1.id, file2);
|
||||
|
||||
const page1 = await fileController.getDelta(session1.id, file1.parent_id, { limit: 1 });
|
||||
expect(page1.has_more).toBe(true);
|
||||
expect(page1.items.length).toBe(1);
|
||||
expect(page1.items[0].type).toBe(ChangeType.Create);
|
||||
expect(page1.items[0].item.id).toBe(file1.id);
|
||||
|
||||
const page2 = await fileController.getDelta(session1.id, file1.parent_id, { cursor: page1.cursor, limit: 1 });
|
||||
expect(page2.has_more).toBe(true);
|
||||
expect(page2.items.length).toBe(1);
|
||||
expect(page2.items[0].type).toBe(ChangeType.Create);
|
||||
expect(page2.items[0].item.id).toBe(file2.id);
|
||||
|
||||
const page3 = await fileController.getDelta(session1.id, file1.parent_id, { cursor: page2.cursor, limit: 1 });
|
||||
expect(page3.has_more).toBe(false);
|
||||
expect(page3.items.length).toBe(0);
|
||||
});
|
||||
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { createUser, checkThrowAsync, beforeAllDb, afterAllDb, beforeEachDb, controllers } from '../../utils/testing/testUtils';
|
||||
import { createUser, checkThrowAsync, beforeAllDb, afterAllTests, beforeEachDb, controllers } from '../../utils/testing/testUtils';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
|
||||
describe('SessionController', function() {
|
||||
@ -8,7 +8,7 @@ describe('SessionController', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { models, controllers, createUserAndSession, checkThrowAsync, beforeAllDb, afterAllDb, beforeEachDb } from '../../utils/testing/testUtils';
|
||||
import { models, controllers, createUserAndSession, checkThrowAsync, beforeAllDb, afterAllTests, beforeEachDb } from '../../utils/testing/testUtils';
|
||||
import { File, User } from '../../db';
|
||||
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
||||
|
||||
@ -9,7 +9,7 @@ describe('UserController', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, models, koaAppContext, koaNext } from '../utils/testing/testUtils';
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, koaNext } from '../utils/testing/testUtils';
|
||||
import { defaultAdminEmail, defaultAdminPassword, Notification } from '../db';
|
||||
import notificationHandler from './notificationHandler';
|
||||
|
||||
@ -9,7 +9,7 @@ describe('notificationHandler', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, koaAppContext, koaNext } from '../utils/testing/testUtils';
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext } from '../utils/testing/testUtils';
|
||||
import ownerHandler from './ownerHandler';
|
||||
|
||||
describe('ownerHandler', function() {
|
||||
@ -8,7 +8,7 @@ describe('ownerHandler', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, models, expectThrow } from '../utils/testing/testUtils';
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, expectThrow } from '../utils/testing/testUtils';
|
||||
import { ChangeType, File } from '../db';
|
||||
import FileModel from './FileModel';
|
||||
import { msleep } from '../utils/time';
|
||||
@ -18,7 +18,7 @@ describe('ChangeModel', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, models, createFileTree } from '../utils/testing/testUtils';
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, createFileTree } from '../utils/testing/testUtils';
|
||||
import { File } from '../db';
|
||||
|
||||
describe('FileModel', function() {
|
||||
@ -8,7 +8,7 @@ describe('FileModel', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, models, expectThrow } from '../utils/testing/testUtils';
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, expectThrow } from '../utils/testing/testUtils';
|
||||
import { Notification, NotificationLevel } from '../db';
|
||||
|
||||
describe('NotificationModel', function() {
|
||||
@ -8,7 +8,7 @@ describe('NotificationModel', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -20,6 +20,15 @@ export interface Pagination {
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
|
||||
interface PaginationQueryParams {
|
||||
limit?: number;
|
||||
order_by?: string;
|
||||
order_dir?: string;
|
||||
page?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResults {
|
||||
items: any[];
|
||||
has_more: boolean;
|
||||
@ -107,6 +116,25 @@ export function requestChangePagination(query: any): ChangePagination {
|
||||
return output;
|
||||
}
|
||||
|
||||
export function paginationToQueryParams(pagination: Pagination): PaginationQueryParams {
|
||||
const output: PaginationQueryParams = {};
|
||||
if (!pagination) return {};
|
||||
|
||||
if ('limit' in pagination) output.limit = pagination.limit;
|
||||
if ('page' in pagination) output.page = pagination.page;
|
||||
if ('cursor' in pagination) output.cursor = pagination.cursor;
|
||||
|
||||
if ('order' in pagination) {
|
||||
const o = pagination.order;
|
||||
if (o.length) {
|
||||
output.order_by = o[0].by;
|
||||
output.order_dir = o[0].dir;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export interface PageLink {
|
||||
page?: number;
|
||||
isEllipsis?: boolean;
|
||||
|
@ -1,12 +1,36 @@
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { testAssetDir, beforeAllDb, afterAllDb, beforeEachDb, koaAppContext, createUserAndSession, models } from '../../utils/testing/testUtils';
|
||||
import { testAssetDir, beforeAllDb, randomHash, afterAllTests, beforeEachDb, createUserAndSession, models, tempDir } from '../../utils/testing/testUtils';
|
||||
import { getFileMetadataContext, getFileMetadata, deleteFileContent, deleteFileContext, deleteFile, postDirectoryContext, postDirectory, getDirectoryChildren, putFileContentContext, putFileContent, getFileContent, patchFileContext, patchFile, getDelta } from '../../utils/testing/apiUtils';
|
||||
import * as fs from 'fs-extra';
|
||||
import { ChangeType, File } from '../../db';
|
||||
import { Pagination, PaginationOrderDir } from '../../models/utils/pagination';
|
||||
import { ErrorUnprocessableEntity, ErrorForbidden, ErrorNotFound, ErrorConflict } from '../../utils/errors';
|
||||
import { msleep } from '../../utils/time';
|
||||
|
||||
function testFilePath(ext: string = 'jpg') {
|
||||
const basename = ext === 'jpg' ? 'photo' : 'poster';
|
||||
return `${testAssetDir}/${basename}.${ext}`;
|
||||
}
|
||||
|
||||
async function makeTempFileWithContent(content: string): Promise<string> {
|
||||
const d = await tempDir();
|
||||
const filePath = `${d}/${randomHash()}`;
|
||||
await fs.writeFile(filePath, content, 'utf8');
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function makeTestFile(ownerId: string, id: number = 1, ext: string = 'jpg', parentId: string = ''): Promise<File> {
|
||||
const basename = ext === 'jpg' ? 'photo' : 'poster';
|
||||
|
||||
const file: File = {
|
||||
name: id > 1 ? `${basename}-${id}.${ext}` : `${basename}.${ext}`,
|
||||
content: await fs.readFile(`${testAssetDir}/${basename}.${ext}`),
|
||||
// mime_type: `image/${ext}`,
|
||||
parent_id: parentId,
|
||||
};
|
||||
|
||||
return models().file({ userId: ownerId }).save(file);
|
||||
}
|
||||
|
||||
describe('api_files', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -14,7 +38,7 @@ describe('api_files', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -25,18 +49,7 @@ describe('api_files', function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
const filePath = testFilePath();
|
||||
|
||||
const context = await koaAppContext({
|
||||
sessionId: session.id,
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '/api/files/root:/photo.jpg:/content',
|
||||
files: { file: { path: filePath } },
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
|
||||
const newFile = context.response.body;
|
||||
const newFile = await putFileContent(session.id, 'root:/photo.jpg:', filePath);
|
||||
|
||||
expect(!!newFile.id).toBe(true);
|
||||
expect(newFile.name).toBe('photo.jpg');
|
||||
@ -60,53 +73,347 @@ describe('api_files', function() {
|
||||
test('should create sub-directories', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const context1 = await koaAppContext({
|
||||
sessionId: session.id,
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/api/files/root/children',
|
||||
body: {
|
||||
is_directory: 1,
|
||||
name: 'subdir',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context1);
|
||||
|
||||
const newDir = context1.response.body;
|
||||
const newDir = await postDirectory(session.id, 'root:/:', 'subdir');
|
||||
expect(!!newDir.id).toBe(true);
|
||||
expect(newDir.is_directory).toBe(1);
|
||||
|
||||
const context2 = await koaAppContext({
|
||||
sessionId: session.id,
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/api/files/root:/subdir:/children',
|
||||
body: {
|
||||
is_directory: 1,
|
||||
name: 'subdir2',
|
||||
},
|
||||
},
|
||||
});
|
||||
const newDir2 = await postDirectory(session.id, 'root:/subdir:', 'subdir2');
|
||||
|
||||
await routeHandler(context2);
|
||||
|
||||
const newDir2 = context2.response.body;
|
||||
|
||||
const context3 = await koaAppContext({
|
||||
sessionId: session.id,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/api/files/root:/subdir/subdir2:',
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context3);
|
||||
|
||||
const newDirReload2 = context3.response.body;
|
||||
const newDirReload2 = await getFileMetadata(session.id, 'root:/subdir/subdir2:');
|
||||
expect(newDirReload2.id).toBe(newDir2.id);
|
||||
expect(newDirReload2.name).toBe(newDir2.name);
|
||||
});
|
||||
|
||||
test('should create files in sub-directory', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
await postDirectory(session.id, 'root:/:', 'subdir');
|
||||
|
||||
const newFile = await putFileContent(session.id, 'root:/subdir/photo.jpg:', testFilePath());
|
||||
|
||||
const newFileReload = await getFileMetadata(session.id, 'root:/subdir/photo.jpg:');
|
||||
expect(newFileReload.id).toBe(newFile.id);
|
||||
expect(newFileReload.name).toBe('photo.jpg');
|
||||
});
|
||||
|
||||
test('should not create a file with an invalid path', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const context = await putFileContentContext(session.id, 'root:/does/not/exist/photo.jpg:', testFilePath());
|
||||
expect(context.response.status).toBe(ErrorNotFound.httpCode);
|
||||
});
|
||||
|
||||
test('should get files', async function() {
|
||||
const { session: session1, user: user1 } = await createUserAndSession(1);
|
||||
const { user: user2 } = await createUserAndSession(2);
|
||||
|
||||
let file1: File = await makeTestFile(user1.id, 1);
|
||||
const file2: File = await makeTestFile(user1.id, 2);
|
||||
const file3: File = await makeTestFile(user2.id, 3);
|
||||
|
||||
const fileId1 = file1.id;
|
||||
const fileId2 = file2.id;
|
||||
|
||||
// Can't get someone else file
|
||||
const context = await getFileMetadataContext(session1.id, file3.id);
|
||||
expect(context.response.status).toBe(ErrorForbidden.httpCode);
|
||||
|
||||
file1 = await getFileMetadata(session1.id, file1.id);
|
||||
expect(file1.id).toBe(fileId1);
|
||||
|
||||
const fileModel = models().file({ userId: user1.id });
|
||||
const paginatedResults = await getDirectoryChildren(session1.id, await fileModel.userRootFileId());
|
||||
const allFiles: File[] = paginatedResults.items;
|
||||
expect(allFiles.length).toBe(2);
|
||||
expect(JSON.stringify(allFiles.map(f => f.id).sort())).toBe(JSON.stringify([fileId1, fileId2].sort()));
|
||||
});
|
||||
|
||||
test('should not let create a file in a directory not owned by user', async function() {
|
||||
const { session: session1 } = await createUserAndSession(1);
|
||||
const { session: session2 } = await createUserAndSession(2);
|
||||
|
||||
const file = await putFileContent(session2.id, 'root:/test.jpg:', testFilePath());
|
||||
const context = await getFileMetadataContext(session1.id, file.id);
|
||||
expect(context.response.status).toBe(ErrorForbidden.httpCode);
|
||||
});
|
||||
|
||||
test('should update file properties', async function() {
|
||||
const { session, user } = await createUserAndSession(1, true);
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
let file = await putFileContent(session.id, 'root:/test.jpg:', testFilePath());
|
||||
|
||||
// Can't have file with empty name
|
||||
const context = await patchFileContext(session.id, file.id, { name: '' });
|
||||
expect(context.response.status).toBe(ErrorUnprocessableEntity.httpCode);
|
||||
|
||||
await patchFile(session.id, file.id, { name: 'modified.jpg' });
|
||||
file = await fileModel.load(file.id);
|
||||
expect(file.name).toBe('modified.jpg');
|
||||
|
||||
await patchFile(session.id, file.id, { mime_type: 'image/png' });
|
||||
file = await fileModel.load(file.id);
|
||||
expect(file.mime_type).toBe('image/png');
|
||||
});
|
||||
|
||||
test('should not allow duplicate filenames', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const c1 = await postDirectoryContext(session.id, 'root:/:', 'mydir');
|
||||
expect(c1.response.status).toBe(200);
|
||||
|
||||
const c2 = await postDirectoryContext(session.id, 'root:/:', 'mydir');
|
||||
expect(c2.response.status).toBe(ErrorConflict.httpCode);
|
||||
});
|
||||
|
||||
test('should change the file parent', async function() {
|
||||
const { session: session1, user: user1 } = await createUserAndSession(1);
|
||||
const { user: user2 } = await createUserAndSession(2);
|
||||
|
||||
const fileModel = models().file({ userId: user1.id });
|
||||
|
||||
let file: File = await makeTestFile(user1.id);
|
||||
const file2: File = await makeTestFile(user1.id, 2);
|
||||
const dir: File = await postDirectory(session1.id, 'root', 'mydir');
|
||||
|
||||
// Can't set parent to another non-directory file
|
||||
const context1 = await patchFileContext(session1.id, file.id, { parent_id: file2.id });
|
||||
expect(context1.response.status).toBe(ErrorForbidden.httpCode);
|
||||
|
||||
// Can't set parent to someone else directory
|
||||
const fileModel2 = models().file({ userId: user2.id });
|
||||
const userRoot2 = await fileModel2.userRootFile();
|
||||
const context2 = await patchFileContext(session1.id, file.id, { parent_id: userRoot2.id });
|
||||
expect(context2.response.status).toBe(ErrorForbidden.httpCode);
|
||||
|
||||
// Finally, change the parent
|
||||
await patchFile(session1.id, file.id, { parent_id: dir.id });
|
||||
file = await fileModel.load(file.id);
|
||||
expect(!!file.parent_id).toBe(true);
|
||||
expect(file.parent_id).toBe(dir.id);
|
||||
});
|
||||
|
||||
test('should delete a file', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
await makeTestFile(user.id, 1);
|
||||
const file2: File = await makeTestFile(user.id, 2);
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
let allFiles: File[] = await fileModel.all();
|
||||
const beforeCount: number = allFiles.length;
|
||||
|
||||
await deleteFile(session.id, file2.id);
|
||||
allFiles = await fileModel.all();
|
||||
expect(allFiles.length).toBe(beforeCount - 1);
|
||||
});
|
||||
|
||||
test('should create and delete directories', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
const dir1 = await postDirectory(session.id, 'root', 'dir1');
|
||||
const dir2 = await postDirectory(session.id, dir1.id, 'dir2');
|
||||
|
||||
const dirReload2: File = await getFileMetadata(session.id, 'root:/dir1/dir2:');
|
||||
expect(dirReload2.id).toBe(dir2.id);
|
||||
|
||||
// Delete one directory
|
||||
await deleteFile(session.id, 'root:/dir1/dir2:');
|
||||
const dirNotFoundContext = await getFileMetadataContext(session.id, 'root:/dir1/dir2:');
|
||||
expect(dirNotFoundContext.response.status).toBe(ErrorNotFound.httpCode);
|
||||
|
||||
// Delete a directory and its sub-directories and files
|
||||
const dir3 = await postDirectory(session.id, 'root:/dir1:', 'dir3');
|
||||
const file1 = await putFileContent(session.id, 'root:/dir1/file1:', testFilePath());
|
||||
const file2 = await putFileContent(session.id, 'root:/dir1/dir3/file2:', testFilePath());
|
||||
await deleteFile(session.id, 'root:/dir1:');
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
expect(!(await fileModel.load(dir1.id))).toBe(true);
|
||||
expect(!(await fileModel.load(dir3.id))).toBe(true);
|
||||
expect(!(await fileModel.load(file1.id))).toBe(true);
|
||||
expect(!(await fileModel.load(file2.id))).toBe(true);
|
||||
});
|
||||
|
||||
test('should not change the parent when updating a file', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
const dir1: File = await postDirectory(session.id, 'root', 'dir1');
|
||||
const file1: File = await putFileContent(session.id, 'root:/dir1/myfile.md:', await makeTempFileWithContent('testing'));
|
||||
|
||||
await putFileContent(session.id, 'root:/dir1/myfile.md:', await makeTempFileWithContent('new content'));
|
||||
|
||||
const fileReloaded1 = await fileModel.load(file1.id);
|
||||
expect(fileReloaded1.parent_id).toBe(dir1.id);
|
||||
});
|
||||
|
||||
test('should not delete someone else file', async function() {
|
||||
const { session: session1 } = await createUserAndSession(1);
|
||||
const { user: user2 } = await createUserAndSession(2);
|
||||
|
||||
const file2: File = await makeTestFile(user2.id, 2);
|
||||
|
||||
const context = await deleteFileContext(session1.id, file2.id);
|
||||
expect(context.response.status).toBe(ErrorForbidden.httpCode);
|
||||
});
|
||||
|
||||
test('should let admin change or delete files', async function() {
|
||||
const { session: adminSession } = await createUserAndSession(1, true);
|
||||
const { user } = await createUserAndSession(2);
|
||||
|
||||
let file: File = await makeTestFile(user.id);
|
||||
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
await patchFile(adminSession.id, file.id, { name: 'modified.jpg' });
|
||||
file = await fileModel.load(file.id);
|
||||
expect(file.name).toBe('modified.jpg');
|
||||
|
||||
await deleteFile(adminSession.id, file.id);
|
||||
expect(!(await fileModel.load(file.id))).toBe(true);
|
||||
});
|
||||
|
||||
test('should update a file content', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const contentPath1 = await makeTempFileWithContent('test1');
|
||||
const contentPath2 = await makeTempFileWithContent('test2');
|
||||
|
||||
await putFileContent(session.id, 'root:/file.txt:', contentPath1);
|
||||
|
||||
const originalContent = (await getFileContent(session.id, 'root:/file.txt:')).toString();
|
||||
expect(originalContent).toBe('test1');
|
||||
|
||||
await putFileContent(session.id, 'root:/file.txt:', contentPath2);
|
||||
const modContent = (await getFileContent(session.id, 'root:/file.txt:')).toString();
|
||||
expect(modContent).toBe('test2');
|
||||
});
|
||||
|
||||
test('should delete a file content', async function() {
|
||||
const { user, session } = await createUserAndSession(1, true);
|
||||
|
||||
const file: File = await makeTestFile(user.id, 1);
|
||||
await putFileContent(session.id, file.id, await makeTempFileWithContent('test1'));
|
||||
|
||||
await deleteFileContent(session.id, file.id);
|
||||
|
||||
const modFile = await getFileMetadata(session.id, file.id);
|
||||
expect(modFile.size).toBe(0);
|
||||
});
|
||||
|
||||
test('should not allow reserved characters', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const filenames = [
|
||||
'invalid*invalid',
|
||||
'invalid#invalid',
|
||||
'invalid\\invalid',
|
||||
];
|
||||
|
||||
for (const filename of filenames) {
|
||||
const context = await putFileContentContext(session.id, `root:/${filename}:`, testFilePath());
|
||||
expect(context.response.status).toBe(ErrorUnprocessableEntity.httpCode);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not allow a directory with the same name', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
{
|
||||
await postDirectory(session.id, 'root', 'somedir');
|
||||
const context = await putFileContentContext(session.id, 'root:/somedir:', testFilePath());
|
||||
expect(context.response.status).toBe(ErrorUnprocessableEntity.httpCode);
|
||||
}
|
||||
|
||||
{
|
||||
await putFileContent(session.id, 'root:/somefile.md:', testFilePath());
|
||||
const context = await postDirectoryContext(session.id, 'root', 'somefile.md');
|
||||
expect(context.response.status).toBe(ErrorConflict.httpCode);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not be possible to delete the root directory', async function() {
|
||||
const { session } = await createUserAndSession(1, true);
|
||||
|
||||
const context = await deleteFileContext(session.id, 'root');
|
||||
expect(context.response.status).toBe(ErrorForbidden.httpCode);
|
||||
});
|
||||
|
||||
test('should support root:/: format, which means root', async function() {
|
||||
const { session, user } = await createUserAndSession(1, true);
|
||||
const fileModel = models().file({ userId: user.id });
|
||||
|
||||
const root = await getFileMetadata(session.id, 'root:/:');
|
||||
expect(root.id).toBe(await fileModel.userRootFileId());
|
||||
});
|
||||
|
||||
test('should paginate results', async function() {
|
||||
const { session: session1, user: user1 } = await createUserAndSession(1);
|
||||
|
||||
const file1: File = await makeTestFile(user1.id, 1);
|
||||
await msleep(1);
|
||||
const file2: File = await makeTestFile(user1.id, 2);
|
||||
await msleep(1);
|
||||
const file3: File = await makeTestFile(user1.id, 3);
|
||||
|
||||
const fileModel = models().file({ userId: user1.id });
|
||||
const rootId = await fileModel.userRootFileId();
|
||||
|
||||
const pagination: Pagination = {
|
||||
limit: 2,
|
||||
order: [
|
||||
{
|
||||
by: 'updated_time',
|
||||
dir: PaginationOrderDir.ASC,
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
};
|
||||
|
||||
for (const method of ['page', 'cursor']) {
|
||||
const page1 = await getDirectoryChildren(session1.id, rootId, pagination);
|
||||
expect(page1.items.length).toBe(2);
|
||||
expect(page1.has_more).toBe(true);
|
||||
expect(page1.items[0].id).toBe(file1.id);
|
||||
expect(page1.items[1].id).toBe(file2.id);
|
||||
|
||||
const p = method === 'page' ? { ...pagination, page: 2 } : { cursor: page1.cursor };
|
||||
const page2 = await getDirectoryChildren(session1.id, rootId, p);
|
||||
expect(page2.items.length).toBe(1);
|
||||
expect(page2.has_more).toBe(false);
|
||||
expect(page2.items[0].id).toBe(file3.id);
|
||||
}
|
||||
});
|
||||
|
||||
test('should track file changes', async function() {
|
||||
// We only do a basic check because most of the tests for this are in
|
||||
// ChangeModel.test.ts
|
||||
|
||||
const { user, session: session1 } = await createUserAndSession(1);
|
||||
|
||||
const file1: File = await makeTestFile(user.id, 1);
|
||||
await msleep(1);
|
||||
const file2: File = await makeTestFile(user.id, 2);
|
||||
|
||||
const page1 = await getDelta(session1.id, file1.parent_id, { limit: 1 });
|
||||
expect(page1.has_more).toBe(true);
|
||||
expect(page1.items.length).toBe(1);
|
||||
expect(page1.items[0].type).toBe(ChangeType.Create);
|
||||
expect(page1.items[0].item.id).toBe(file1.id);
|
||||
|
||||
const page2 = await getDelta(session1.id, file1.parent_id, { cursor: page1.cursor, limit: 1 });
|
||||
expect(page2.has_more).toBe(true);
|
||||
expect(page2.items.length).toBe(1);
|
||||
expect(page2.items[0].type).toBe(ChangeType.Create);
|
||||
expect(page2.items[0].item.id).toBe(file2.id);
|
||||
|
||||
const page3 = await getDelta(session1.id, file1.parent_id, { cursor: page2.cursor, limit: 1 });
|
||||
expect(page3.has_more).toBe(false);
|
||||
expect(page3.items.length).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { beforeAllDb, afterAllDb, beforeEachDb, koaAppContext } from '../../utils/testing/testUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext } from '../../utils/testing/testUtils';
|
||||
|
||||
describe('api_ping', function() {
|
||||
|
||||
@ -8,7 +8,7 @@ describe('api_ping', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Session } from '../../db';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { beforeAllDb, afterAllDb, beforeEachDb, koaAppContext, createUserAndSession, models } from '../../utils/testing/testUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models } from '../../utils/testing/testUtils';
|
||||
|
||||
describe('api_sessions', function() {
|
||||
|
||||
@ -9,7 +9,7 @@ describe('api_sessions', function() {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -2,6 +2,8 @@
|
||||
// https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
|
||||
class ApiError extends Error {
|
||||
public static httpCode: number = 400;
|
||||
|
||||
public httpCode: number;
|
||||
public code: string;
|
||||
public constructor(message: string, httpCode: number = 400, code: string = undefined) {
|
||||
@ -13,51 +15,65 @@ class ApiError extends Error {
|
||||
}
|
||||
|
||||
export class ErrorMethodNotAllowed extends ApiError {
|
||||
public static httpCode: number = 400;
|
||||
|
||||
public constructor(message: string = 'Method Not Allowed') {
|
||||
super(message, 405);
|
||||
super(message, ErrorMethodNotAllowed.httpCode);
|
||||
Object.setPrototypeOf(this, ErrorMethodNotAllowed.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorNotFound extends ApiError {
|
||||
public static httpCode: number = 404;
|
||||
|
||||
public constructor(message: string = 'Not Found') {
|
||||
super(message, 404);
|
||||
super(message, ErrorNotFound.httpCode);
|
||||
Object.setPrototypeOf(this, ErrorNotFound.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorForbidden extends ApiError {
|
||||
public static httpCode: number = 403;
|
||||
|
||||
public constructor(message: string = 'Forbidden') {
|
||||
super(message, 403);
|
||||
super(message, ErrorForbidden.httpCode);
|
||||
Object.setPrototypeOf(this, ErrorForbidden.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorBadRequest extends ApiError {
|
||||
public static httpCode: number = 400;
|
||||
|
||||
public constructor(message: string = 'Bad Request') {
|
||||
super(message, 400);
|
||||
super(message, ErrorBadRequest.httpCode);
|
||||
Object.setPrototypeOf(this, ErrorBadRequest.prototype);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ErrorUnprocessableEntity extends ApiError {
|
||||
public static httpCode: number = 422;
|
||||
|
||||
public constructor(message: string = 'Unprocessable Entity') {
|
||||
super(message, 422);
|
||||
super(message, ErrorUnprocessableEntity.httpCode);
|
||||
Object.setPrototypeOf(this, ErrorUnprocessableEntity.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorConflict extends ApiError {
|
||||
public static httpCode: number = 409;
|
||||
|
||||
public constructor(message: string = 'Conflict') {
|
||||
super(message, 409);
|
||||
super(message, ErrorConflict.httpCode);
|
||||
Object.setPrototypeOf(this, ErrorConflict.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorResyncRequired extends ApiError {
|
||||
public static httpCode: number = 400;
|
||||
|
||||
public constructor(message: string = 'Delta cursor is invalid and the complete data should be resynced') {
|
||||
super(message, 400, 'resyncRequired');
|
||||
super(message, ErrorResyncRequired.httpCode, 'resyncRequired');
|
||||
Object.setPrototypeOf(this, ErrorResyncRequired.prototype);
|
||||
}
|
||||
}
|
||||
|
194
packages/server/src/utils/testing/apiUtils.ts
Normal file
194
packages/server/src/utils/testing/apiUtils.ts
Normal file
@ -0,0 +1,194 @@
|
||||
// These utility functions allow making API calls easily from test units.
|
||||
// There's two versions of each function:
|
||||
//
|
||||
// - A regular one, eg. "postDirectory", which returns whatever would have
|
||||
// normally return the API call. It also checks for error.
|
||||
//
|
||||
// - The other function is suffixed with "Context", eg "postDirectoryContext".
|
||||
// In that case, it returns the complete Koa context, which can be used in
|
||||
// particular to access the response object and test for errors.
|
||||
|
||||
import { File } from '../../db';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { PaginatedResults, Pagination, paginationToQueryParams } from '../../models/utils/pagination';
|
||||
import { AppContext } from '../types';
|
||||
import { koaAppContext } from './testUtils';
|
||||
|
||||
function checkContextError(context: AppContext) {
|
||||
if (context.response.status >= 400) throw new Error(`Cannot create directory: ${JSON.stringify(context.response)}`);
|
||||
}
|
||||
|
||||
export async function getFileMetadataContext(sessionId: string, path: string): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: `/api/files/${path}`,
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function getFileMetadata(sessionId: string, path: string): Promise<File> {
|
||||
const context = await getFileMetadataContext(sessionId, path);
|
||||
checkContextError(context);
|
||||
return context.response.body;
|
||||
}
|
||||
|
||||
export async function deleteFileContentContext(sessionId: string, path: string): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: `/api/files/${path}/content`,
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function deleteFileContent(sessionId: string, path: string): Promise<void> {
|
||||
const context = await deleteFileContentContext(sessionId, path);
|
||||
checkContextError(context);
|
||||
}
|
||||
|
||||
export async function deleteFileContext(sessionId: string, path: string): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: `/api/files/${path}`,
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function deleteFile(sessionId: string, path: string): Promise<void> {
|
||||
const context = await deleteFileContext(sessionId, path);
|
||||
checkContextError(context);
|
||||
}
|
||||
|
||||
export async function postDirectoryContext(sessionId: string, parentPath: string, name: string): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: `/api/files/${parentPath}/children`,
|
||||
body: {
|
||||
is_directory: 1,
|
||||
name: name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function postDirectory(sessionId: string, parentPath: string, name: string): Promise<File> {
|
||||
const context = await postDirectoryContext(sessionId, parentPath, name);
|
||||
checkContextError(context);
|
||||
return context.response.body;
|
||||
}
|
||||
|
||||
export async function getDirectoryChildrenContext(sessionId: string, path: string, pagination: Pagination = null): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: `/api/files/${path}/children`,
|
||||
query: paginationToQueryParams(pagination),
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function getDirectoryChildren(sessionId: string, path: string, pagination: Pagination = null): Promise<PaginatedResults> {
|
||||
const context = await getDirectoryChildrenContext(sessionId, path, pagination);
|
||||
checkContextError(context);
|
||||
return context.response.body;
|
||||
}
|
||||
|
||||
export async function putFileContentContext(sessionId: string, path: string, filePath: string): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: `/api/files/${path}/content`,
|
||||
files: { file: { path: filePath } },
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function putFileContent(sessionId: string, path: string, filePath: string): Promise<File> {
|
||||
const context = await putFileContentContext(sessionId, path, filePath);
|
||||
checkContextError(context);
|
||||
return context.response.body;
|
||||
}
|
||||
|
||||
export async function getFileContentContext(sessionId: string, path: string): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: `/api/files/${path}/content`,
|
||||
},
|
||||
});
|
||||
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function getFileContent(sessionId: string, path: string): Promise<Buffer> {
|
||||
const context = await getFileContentContext(sessionId, path);
|
||||
checkContextError(context);
|
||||
return context.response.body;
|
||||
}
|
||||
|
||||
export async function patchFileContext(sessionId: string, path: string, file: File): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'PATCH',
|
||||
url: `/api/files/${path}`,
|
||||
body: file,
|
||||
},
|
||||
});
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function patchFile(sessionId: string, path: string, file: File): Promise<File> {
|
||||
const context = await patchFileContext(sessionId, path, file);
|
||||
checkContextError(context);
|
||||
return context.response.body;
|
||||
}
|
||||
|
||||
export async function getDeltaContext(sessionId: string, path: string, pagination: Pagination): Promise<AppContext> {
|
||||
const context = await koaAppContext({
|
||||
sessionId: sessionId,
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: `/api/files/${path}/delta`,
|
||||
query: paginationToQueryParams(pagination),
|
||||
},
|
||||
});
|
||||
await routeHandler(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function getDelta(sessionId: string, path: string, pagination: Pagination): Promise<PaginatedResults> {
|
||||
const context = await getDeltaContext(sessionId, path, pagination);
|
||||
checkContextError(context);
|
||||
return context.response.body;
|
||||
}
|
@ -11,6 +11,8 @@ import FakeCookies from './koa/FakeCookies';
|
||||
import FakeRequest from './koa/FakeRequest';
|
||||
import FakeResponse from './koa/FakeResponse';
|
||||
import * as httpMocks from 'node-mocks-http';
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
// Takes into account the fact that this file will be inside the /dist directory
|
||||
// when it runs.
|
||||
@ -20,6 +22,18 @@ let db_: DbConnection = null;
|
||||
|
||||
// require('source-map-support').install();
|
||||
|
||||
export function randomHash(): string {
|
||||
return crypto.createHash('md5').update(`${Date.now()}-${Math.random()}`).digest('hex');
|
||||
}
|
||||
|
||||
let tempDir_: string = null;
|
||||
export async function tempDir(): Promise<string> {
|
||||
if (tempDir_) return tempDir_;
|
||||
tempDir_ = `${packageRootDir}/temp/${randomHash()}`;
|
||||
await fs.mkdirp(tempDir_);
|
||||
return tempDir_;
|
||||
}
|
||||
|
||||
export async function beforeAllDb(unitName: string) {
|
||||
const config: Config = {
|
||||
...baseConfig,
|
||||
@ -34,9 +48,16 @@ export async function beforeAllDb(unitName: string) {
|
||||
db_ = await connectDb(config.database);
|
||||
}
|
||||
|
||||
export async function afterAllDb() {
|
||||
await disconnectDb(db_);
|
||||
db_ = null;
|
||||
export async function afterAllTests() {
|
||||
if (db_) {
|
||||
await disconnectDb(db_);
|
||||
db_ = null;
|
||||
}
|
||||
|
||||
if (tempDir_) {
|
||||
await fs.remove(tempDir_);
|
||||
tempDir_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function beforeEachDb() {
|
||||
@ -98,6 +119,7 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom
|
||||
appContext.response = new FakeResponse();
|
||||
appContext.headers = { ...reqOptions.headers };
|
||||
appContext.req = req;
|
||||
appContext.query = req.query;
|
||||
appContext.method = req.method;
|
||||
|
||||
if (options.sessionId) {
|
||||
|
Loading…
Reference in New Issue
Block a user