diff --git a/packages/server/src/controllers/api/FileController.test.ts b/packages/server/src/controllers/api/FileController.test.ts deleted file mode 100644 index cf9a824ac..000000000 --- a/packages/server/src/controllers/api/FileController.test.ts +++ /dev/null @@ -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 { - 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 { - const file: File = { - name: name, - parent_id: '', - is_directory: 1, - }; - - return file; -} - -async function saveTestFile(sessionId: string, path: string): Promise { - const fileController = controllers().apiFile(); - - return fileController.putFileContent( - sessionId, - path, - null - ); -} - -async function saveTestDir(sessionId: string, path: string): Promise { - 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); - }); - -}); diff --git a/packages/server/src/controllers/api/SessionController.test.ts b/packages/server/src/controllers/api/SessionController.test.ts index 01801d2b5..56cc1b868 100644 --- a/packages/server/src/controllers/api/SessionController.test.ts +++ b/packages/server/src/controllers/api/SessionController.test.ts @@ -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 () => { diff --git a/packages/server/src/controllers/api/UserController.test.ts b/packages/server/src/controllers/api/UserController.test.ts index d00d46b5b..57bc75918 100644 --- a/packages/server/src/controllers/api/UserController.test.ts +++ b/packages/server/src/controllers/api/UserController.test.ts @@ -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 () => { diff --git a/packages/server/src/middleware/notificationHandler.test.ts b/packages/server/src/middleware/notificationHandler.test.ts index 146d30d3e..fc1c6e52a 100644 --- a/packages/server/src/middleware/notificationHandler.test.ts +++ b/packages/server/src/middleware/notificationHandler.test.ts @@ -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 () => { diff --git a/packages/server/src/middleware/ownerHandler.test.ts b/packages/server/src/middleware/ownerHandler.test.ts index dce3d0dce..2bd86821a 100644 --- a/packages/server/src/middleware/ownerHandler.test.ts +++ b/packages/server/src/middleware/ownerHandler.test.ts @@ -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 () => { diff --git a/packages/server/src/models/ChangeModel.test.ts b/packages/server/src/models/ChangeModel.test.ts index 8f9d08d9e..13f2c3bb8 100644 --- a/packages/server/src/models/ChangeModel.test.ts +++ b/packages/server/src/models/ChangeModel.test.ts @@ -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 () => { diff --git a/packages/server/src/models/FileModel.test.ts b/packages/server/src/models/FileModel.test.ts index cab1d5508..d080b0f21 100644 --- a/packages/server/src/models/FileModel.test.ts +++ b/packages/server/src/models/FileModel.test.ts @@ -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 () => { diff --git a/packages/server/src/models/NotificationModel.test.ts b/packages/server/src/models/NotificationModel.test.ts index cf0d465e4..b3fe98e63 100644 --- a/packages/server/src/models/NotificationModel.test.ts +++ b/packages/server/src/models/NotificationModel.test.ts @@ -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 () => { diff --git a/packages/server/src/models/utils/pagination.ts b/packages/server/src/models/utils/pagination.ts index 4b4652140..3d5fb7b8c 100644 --- a/packages/server/src/models/utils/pagination.ts +++ b/packages/server/src/models/utils/pagination.ts @@ -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; diff --git a/packages/server/src/routes/api/files.test.ts b/packages/server/src/routes/api/files.test.ts index 288d8674f..1f01c903a 100644 --- a/packages/server/src/routes/api/files.test.ts +++ b/packages/server/src/routes/api/files.test.ts @@ -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 { + 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 { + 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); + }); + }); diff --git a/packages/server/src/routes/api/ping.test.ts b/packages/server/src/routes/api/ping.test.ts index 24a0ae7d6..f08057af6 100644 --- a/packages/server/src/routes/api/ping.test.ts +++ b/packages/server/src/routes/api/ping.test.ts @@ -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 () => { diff --git a/packages/server/src/routes/api/sessions.test.ts b/packages/server/src/routes/api/sessions.test.ts index 1c859d580..477875612 100644 --- a/packages/server/src/routes/api/sessions.test.ts +++ b/packages/server/src/routes/api/sessions.test.ts @@ -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 () => { diff --git a/packages/server/src/utils/errors.ts b/packages/server/src/utils/errors.ts index c8e418d72..2911d0a2e 100644 --- a/packages/server/src/utils/errors.ts +++ b/packages/server/src/utils/errors.ts @@ -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); } } diff --git a/packages/server/src/utils/testing/apiUtils.ts b/packages/server/src/utils/testing/apiUtils.ts new file mode 100644 index 000000000..03e4ddf92 --- /dev/null +++ b/packages/server/src/utils/testing/apiUtils.ts @@ -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 { + 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 { + const context = await getFileMetadataContext(sessionId, path); + checkContextError(context); + return context.response.body; +} + +export async function deleteFileContentContext(sessionId: string, path: string): Promise { + 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 { + const context = await deleteFileContentContext(sessionId, path); + checkContextError(context); +} + +export async function deleteFileContext(sessionId: string, path: string): Promise { + 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 { + const context = await deleteFileContext(sessionId, path); + checkContextError(context); +} + +export async function postDirectoryContext(sessionId: string, parentPath: string, name: string): Promise { + 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 { + 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 { + 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 { + const context = await getDirectoryChildrenContext(sessionId, path, pagination); + checkContextError(context); + return context.response.body; +} + +export async function putFileContentContext(sessionId: string, path: string, filePath: string): Promise { + 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 { + const context = await putFileContentContext(sessionId, path, filePath); + checkContextError(context); + return context.response.body; +} + +export async function getFileContentContext(sessionId: string, path: string): Promise { + 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 { + const context = await getFileContentContext(sessionId, path); + checkContextError(context); + return context.response.body; +} + +export async function patchFileContext(sessionId: string, path: string, file: File): Promise { + 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 { + const context = await patchFileContext(sessionId, path, file); + checkContextError(context); + return context.response.body; +} + +export async function getDeltaContext(sessionId: string, path: string, pagination: Pagination): Promise { + 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 { + const context = await getDeltaContext(sessionId, path, pagination); + checkContextError(context); + return context.response.body; +} diff --git a/packages/server/src/utils/testing/testUtils.ts b/packages/server/src/utils/testing/testUtils.ts index 3a0c5157b..b375c577e 100644 --- a/packages/server/src/utils/testing/testUtils.ts +++ b/packages/server/src/utils/testing/testUtils.ts @@ -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 { + 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) {