🚧 #46 Shared with me (#67)

* 🚧 #46 Shared with me
This commit is contained in:
Anton Shepilov
2023-06-07 10:48:44 +02:00
committed by GitHub
parent 49ec20d60e
commit f1bf507865
42 changed files with 1122 additions and 867 deletions

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,192 @@
// @ts-ignore
import fs from "fs";
import {ResourceUpdateResponse, Workspace} from "../../../src/utils/types";
import {File} from "../../../src/services/files/entities/file";
import {deserialize} from "class-transformer";
import formAutoContent from "form-auto-content";
import {TestPlatform, User} from "../setup";
import {v1 as uuidv1} from "uuid";
import {TestDbService} from "../utils.prepare.db";
import {DriveFile} from "../../../src/services/documents/entities/drive-file";
import {FileVersion} from "../../../src/services/documents/entities/file-version";
import {SearchResultMockClass} from "./entities/mock_entities";
import {logger} from "../../../src/core/platform/framework";
export default class TestHelpers {
private static readonly DOC_URL = "/internal/services/documents/v1";
static readonly ALL_FILES = [
"sample.png",
"sample.gif",
"sample.pdf",
"sample.doc",
"sample.zip",
"sample.mp4",
]
platform: TestPlatform;
dbService: TestDbService;
user: User;
workspace: Workspace;
jwt: string;
private constructor(
platform: TestPlatform,
) {
this.platform = platform
}
private async init(newUser: boolean) {
this.dbService = await TestDbService.getInstance(this.platform, true);
if (newUser) {
this.workspace = this.platform.workspace;
const workspacePK = {id: this.workspace.workspace_id, company_id: this.workspace.company_id};
this.user = await this.dbService.createUser([workspacePK], {}, uuidv1());
} else {
this.user = this.platform.currentUser;
this.workspace = this.platform.workspace;
}
this.jwt = this.getJWTTokenForUser(this.user.id);
}
public static async getInstance(platform: TestPlatform, newUser = false): Promise<TestHelpers> {
const helpers = new TestHelpers(platform);
await helpers.init(newUser)
return helpers;
}
async uploadFiles() {
return Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFile(f)));
}
async uploadRandomFile() {
return await this.uploadFile(TestHelpers.ALL_FILES[Math.floor((Math.random()*TestHelpers.ALL_FILES.length))])
}
async uploadFile(filename: string) {
logger.info(`Upload ${filename} for the user: ${this.user.id}`);
const fullPath = `${__dirname}/assets/${filename}`
const url = "/internal/services/files/v1";
const form = formAutoContent({file: fs.createReadStream(fullPath)});
form.headers["authorization"] = `Bearer ${this.jwt}`;
const filesUploadRaw = await this.platform.app.inject({
method: "POST",
url: `${url}/companies/${this.platform.workspace.company_id}/files?thumbnail_sync=1`,
...form,
});
const filesUpload: ResourceUpdateResponse<File> = deserialize(
ResourceUpdateResponse,
filesUploadRaw.body,
);
return filesUpload.resource;
}
private getJWTTokenForUser(userId: string): string {
const payload = {
sub: userId,
role: "",
}
return this.platform.authService.sign(payload);
}
async uploadFileAndCreateDocument(
filename: string
) {
return this.uploadFile(filename).then(f => this.createDocumentFromFile(f));
};
async uploadRandomFileAndCreateDocument() {
return this.uploadRandomFile().then(f => this.createDocumentFromFile(f));
};
async uploadAllFilesAndCreateDocuments() {
return await Promise.all(TestHelpers.ALL_FILES.map(f => this.uploadFileAndCreateDocument(f)))
};
async uploadAllFilesOneByOne() {
const files: Array<DriveFile> = [];
for (const idx in TestHelpers.ALL_FILES) {
const f = await this.uploadFile(TestHelpers.ALL_FILES[idx]);
const doc = await this.createDocumentFromFile(f);
files.push(doc);
}
return files;
};
async createDocument(
platform: TestPlatform,
item: Partial<DriveFile>,
version: Partial<FileVersion>
) {
return await platform.app.inject({
method: "POST",
url: `${TestHelpers.DOC_URL}/companies/${platform.workspace.company_id}/item`,
headers: {
authorization: `Bearer ${this.jwt}`,
},
payload: {
item,
version,
},
});
};
async createDocumentFromFile(
file: File
) {
const item = {
name: file.metadata.name,
parent_id: "root",
company_id: file.company_id,
};
const version = {
file_metadata: {
name: file.metadata.name,
size: file.upload_data?.size,
thumbnails: [],
external_id: file.id
}
}
const response = await this.createDocument(this.platform, item, version);
return deserialize<DriveFile>(DriveFile, response.body);
};
async updateDocument(
id: string | "root" | "trash",
item: Partial<DriveFile>
) {
return await this.platform.app.inject({
method: "POST",
url: `${TestHelpers.DOC_URL}/companies/${this.platform.workspace.company_id}/item/${id}`,
headers: {
authorization: `Bearer ${this.jwt}`,
},
payload: item,
});
};
async searchDocument (
payload: Record<string, any>
){
const response = await this.platform.app.inject({
method: "POST",
url: `${TestHelpers.DOC_URL}/companies/${this.platform.workspace.company_id}/search`,
headers: {
authorization: `Bearer ${this.jwt}`,
},
payload,
});
return deserialize<SearchResultMockClass>(
SearchResultMockClass,
response.body)
};
}
@@ -0,0 +1,42 @@
import {DriveFileAccessLevel, publicAccessLevel} from "../../../../src/services/documents/types";
export type MockAccessInformation = {
public?: {
token: string;
password: string;
expiration: number;
level: publicAccessLevel;
};
entities: MockAuthEntity[];
};
export type MockAuthEntity = {
type: "user" | "channel" | "company" | "folder";
id: string | "parent";
level: publicAccessLevel | DriveFileAccessLevel;
};
export class DriveFileMockClass {
id: string;
name: string;
size: number;
added: string;
parent_id: string;
extension: string;
description: string;
tags: string[];
last_modified: string;
access_info: MockAccessInformation;
creator: string;
}
export class DriveItemDetailsMockClass {
path: string[];
item: DriveFileMockClass;
children: DriveFileMockClass[];
versions: Record<string, unknown>[];
}
export class SearchResultMockClass {
entities: DriveFileMockClass[];
}
@@ -1,5 +1,5 @@
import { describe, beforeEach, afterEach, it, expect, afterAll } from "@jest/globals";
import { deserialize } from "class-transformer";
import {deserialize} from "class-transformer";
import { File } from "../../../src/services/files/entities/file";
import { ResourceUpdateResponse } from "../../../src/utils/types";
import { init, TestPlatform } from "../setup";
@@ -13,28 +13,13 @@ import {
e2e_searchDocument,
e2e_updateDocument,
} from "./utils";
import TestHelpers from "../common/common_test_helpers";
import {DriveFileMockClass, DriveItemDetailsMockClass, SearchResultMockClass} from "../common/entities/mock_entities";
describe("the Drive feature", () => {
let platform: TestPlatform;
class DriveFileMockClass {
id: string;
name: string;
size: number;
added: string;
parent_id: string;
}
class DriveItemDetailsMockClass {
path: string[];
item: DriveFileMockClass;
children: DriveFileMockClass[];
versions: Record<string, unknown>[];
}
class SearchResultMockClass {
entities: DriveFileMockClass[];
}
let currentUser: TestHelpers;
let dbService: TestDbService;
beforeEach(async () => {
platform = await init({
@@ -59,6 +44,8 @@ describe("the Drive feature", () => {
"documents",
],
});
currentUser = await TestHelpers.getInstance(platform);
dbService = await TestDbService.getInstance(platform, true);
});
afterEach(async () => {
@@ -170,8 +157,8 @@ describe("the Drive feature", () => {
done?.();
});
// TODO: wait for elastic search index
it("did search for an item", async done => {
jest.setTimeout(10000);
const createItemResult = await createItem();
expect(createItemResult.id).toBeDefined();
@@ -237,4 +224,230 @@ describe("the Drive feature", () => {
done?.();
});
it("did search by mime type", async done => {
// given:: all the sample files uploaded and documents for them created
await Promise.all((await currentUser.uploadFiles()).map(f => currentUser.createDocumentFromFile(f)))
const filters = {
mime_type: "application/pdf",
};
jest.setTimeout(10000);
await new Promise(r => setTimeout(r, 5000));
let documents = await currentUser.searchDocument(filters);
expect(documents.entities).toHaveLength(1);
const actualFile = documents.entities[0];
expect(actualFile.name).toEqual("sample.pdf");
done?.();
});
it("did search by last modified", async done => {
jest.setTimeout(10000);
const user = await TestHelpers.getInstance(platform, true);
// given:: all the sample files uploaded and documents for them created
const start = new Date().getTime();
await user.uploadAllFilesOneByOne()
const end = new Date().getTime();
await user.uploadAllFilesOneByOne()
//wait for putting docs to elastic and its indexing
await new Promise(r => setTimeout(r, 3000));
//then:: all the files are searchable without filters
let documents = await user.searchDocument({});
expect(documents.entities).toHaveLength(TestHelpers.ALL_FILES.length * 2);
//then:: only file uploaded in the [start, end] interval are shown in the search results
const filters = {
last_modified_gt: start.toString(),
last_modified_lt: end.toString()
};
documents = await user.searchDocument(filters);
expect(documents.entities).toHaveLength(TestHelpers.ALL_FILES.length);
done?.();
});
it("did search a file shared by another user", async done => {
jest.setTimeout(30000);
//given:
const oneUser = await TestHelpers.getInstance(platform, true);
const anotherUser = await TestHelpers.getInstance(platform, true);
//upload files
let files = await oneUser.uploadAllFilesOneByOne()
await new Promise(r => setTimeout(r, 5000));
//then:: files are not searchable for user without permissions
expect((await anotherUser.searchDocument({})).entities).toHaveLength(0);
//and searchable for user that have
expect((await oneUser.searchDocument({})).entities).toHaveLength(TestHelpers.ALL_FILES.length);
//give permissions to the file
files[0].access_info.entities.push({
type: "user",
id: anotherUser.user.id,
level: "read"
})
await oneUser.updateDocument(files[0].id, files[0]);
await new Promise(r => setTimeout(r, 3000));
//then file become searchable
expect((await anotherUser.searchDocument({})).entities).toHaveLength(1);
done?.();
});
it("did search a file by file owner", async done => {
jest.setTimeout(30000);
//given:
const oneUser = await TestHelpers.getInstance(platform, true);
const anotherUser = await TestHelpers.getInstance(platform, true);
//upload files
let files = await oneUser.uploadAllFilesOneByOne()
await anotherUser.uploadAllFilesOneByOne()
//give permissions for all files to 'another user'
await Promise.all(files.map(f => {
f.access_info.entities.push({
type: "user",
id: anotherUser.user.id,
level: "read"
})
return oneUser.updateDocument(f.id, f);
}));
await new Promise(r => setTimeout(r, 5000));
//then:: all files are searchable for 'another user'
expect((await anotherUser.searchDocument({})).entities).toHaveLength(TestHelpers.ALL_FILES.length * 2);
//and searchable for user that have
expect((await oneUser.searchDocument({
creator: oneUser.user.id,
})).entities).toHaveLength(TestHelpers.ALL_FILES.length);
done?.();
});
it("did search by 'added' date", async done => {
jest.setTimeout(10000);
const user = await TestHelpers.getInstance(platform, true);
// given:: all the sample files uploaded and documents for them created
await user.uploadRandomFileAndCreateDocument();
const start = new Date().getTime();
await user.uploadAllFilesAndCreateDocuments()
const end = new Date().getTime();
await user.uploadRandomFileAndCreateDocument();
//wait for putting docs to elastic and its indexing
await new Promise(r => setTimeout(r, 3000));
//then:: all the files are searchable without filters
let documents = await user.searchDocument({});
expect(documents.entities).toHaveLength(TestHelpers.ALL_FILES.length + 2);
//then:: only file uploaded in the [start, end] interval are shown in the search results
const filters = {
added_gt: start.toString(),
added_lt: end.toString()
};
documents = await user.searchDocument(filters);
expect(documents.entities).toHaveLength(TestHelpers.ALL_FILES.length);
done?.();
});
it("did search order by name", async done => {
jest.setTimeout(10000);
const user = await TestHelpers.getInstance(platform, true);
// given:: all the sample files uploaded and documents for them created
await user.uploadAllFilesAndCreateDocuments()
//wait for putting docs to elastic and its indexing
await new Promise(r => setTimeout(r, 5000));
//when:: sort files by name is ascending order
const options = {
sort: {
name_keyword: "asc",
}
};
const documents = await user.searchDocument(options);
//then all the files are sorted properly by name
expect(documents.entities.map(e => e.name)).toEqual(TestHelpers.ALL_FILES.sort());
done?.();
});
it("did search order by name desc", async done => {
jest.setTimeout(10000);
const user = await TestHelpers.getInstance(platform, true);
// given:: all the sample files uploaded and documents for them created
await user.uploadAllFilesOneByOne()
//wait for putting docs to elastic and its indexing
await new Promise(r => setTimeout(r, 5000));
//when:: sort files by name is ascending order
const options = {
sort: {
name_keyword: "desc",
}
};
const documents = await user.searchDocument(options);
//then all the files are sorted properly by name
expect(documents.entities.map(e => e.name)).toEqual(TestHelpers.ALL_FILES.sort().reverse());
done?.();
});
it("did search order by added date", async done => {
jest.setTimeout(10000);
const user = await TestHelpers.getInstance(platform, true);
// given:: all the sample files uploaded and documents for them created
await user.uploadAllFilesOneByOne();
//wait for putting docs to elastic and its indexing
await new Promise(r => setTimeout(r, 5000));
//when:: ask to sort files by the 'added' field
const options = {
sort: {
added: "asc",
}
};
const documents = await user.searchDocument(options);
//then:: files should be sorted properly
expect(documents.entities.map(e => e.name)).toEqual(TestHelpers.ALL_FILES);
done?.();
});
it("did search order by added date desc", async done => {
jest.setTimeout(10000);
const user = await TestHelpers.getInstance(platform, true);
// given:: all the sample files uploaded and documents for them created
await user.uploadAllFilesOneByOne();
//wait for putting docs to elastic and its indexing
await new Promise(r => setTimeout(r, 5000));
//when:: ask to sort files by the 'added' field desc
const options = {
sort: {
added: "desc",
}
};
const documents = await user.searchDocument(options);
//then:: files should be sorted properly
expect(documents.entities.map(e => e.name)).toEqual(TestHelpers.ALL_FILES.reverse());
done?.();
});
});
@@ -1,25 +1,24 @@
import "reflect-metadata";
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
import { init, TestPlatform } from "../setup";
import { ResourceUpdateResponse } from "../../../src/utils/types";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import fs from "fs";
import { File } from "../../../src/services/files/entities/file";
import { deserialize } from "class-transformer";
import formAutoContent from "form-auto-content";
import LocalConnectorService from "../../../src/core/platform/services/storage/connectors/local/service";
import TestHelpers from "../common/common_test_helpers";
describe("The Files feature", () => {
const url = "/internal/services/files/v1";
let platform: TestPlatform;
let helpers: TestHelpers;
beforeAll(async () => {
platform = await init({
services: ["webserver", "database", "storage", "files", "previews"],
});
await platform.database.getConnector().init();
helpers = await TestHelpers.getInstance(platform)
});
afterAll(async done => {
@@ -28,37 +27,13 @@ describe("The Files feature", () => {
done();
});
async function uploadFile(file: string) {
const form = formAutoContent({file: fs.createReadStream(file)});
form.headers["authorization"] = `Bearer ${await platform.auth.getJWTToken()}`;
const filesUploadRaw = await platform.app.inject({
method: "POST",
url: `${url}/companies/${platform.workspace.company_id}/files?thumbnail_sync=1`,
...form,
});
const filesUpload: ResourceUpdateResponse<File> = deserialize(
ResourceUpdateResponse,
filesUploadRaw.body,
);
return filesUpload;
}
describe("On user send files", () => {
const files = [
"assets/sample.png",
"assets/sample.gif",
"assets/sample.pdf",
"assets/sample.doc",
"assets/sample.zip",
"assets/sample.mp4",
].map(p => `${__dirname}/${p}`);
const thumbnails = [1, 1, 2, 5, 0, 1];
it("Download file should return 500 if file doesn't exists", async () => {
//given file
const filesUpload = await uploadFile(files[0]);
expect(filesUpload.resource.id).toBeTruthy();
const filesUpload = await helpers.uploadRandomFile();
expect(filesUpload.id).toBeTruthy();
//clean files directory
expect(platform.storage.getConnector()).toBeInstanceOf(LocalConnectorService)
const path = (<LocalConnectorService>platform.storage.getConnector()).configuration.path;
@@ -66,7 +41,7 @@ describe("The Files feature", () => {
//when try to download the file
const fileDownloadResponse = await platform.app.inject({
method: "GET",
url: `${url}/companies/${platform.workspace.company_id}/files/${filesUpload.resource.id}/download`,
url: `${url}/companies/${platform.workspace.company_id}/files/${filesUpload.id}/download`,
});
//then file should be not found with 404 error and "File not found message"
expect(fileDownloadResponse).toBeTruthy();
@@ -76,15 +51,15 @@ describe("The Files feature", () => {
it("Download file should return 200 if file exists", async () => {
//given file
const filesUpload = await uploadFile(files[0]);
expect(filesUpload.resource.id).toBeTruthy();
const filesUpload = await helpers.uploadRandomFile()
expect(filesUpload.id).toBeTruthy();
//clean files directory
expect(platform.storage.getConnector()).toBeInstanceOf(LocalConnectorService)
//when try to download the file
const fileDownloadResponse = await platform.app.inject({
method: "GET",
url: `${url}/companies/${platform.workspace.company_id}/files/${filesUpload.resource.id}/download`,
url: `${url}/companies/${platform.workspace.company_id}/files/${filesUpload.id}/download`,
});
//then file should be not found with 404 error and "File not found message"
expect(fileDownloadResponse).toBeTruthy();
@@ -94,20 +69,20 @@ describe("The Files feature", () => {
it.skip("should save file and generate previews", async done => {
for (const i in files) {
const file = files[i];
for (const i in TestHelpers.ALL_FILES) {
const file = TestHelpers.ALL_FILES[i];
const filesUpload = await uploadFile(file);
const filesUpload = await helpers.uploadFile(file);
expect(filesUpload.resource.id).not.toBeFalsy();
expect(filesUpload.resource.encryption_key).toBeFalsy(); //This must not be disclosed
expect(filesUpload.resource.thumbnails.length).toBe(thumbnails[i]);
expect(filesUpload.id).not.toBeFalsy();
expect(filesUpload.encryption_key).toBeFalsy(); //This must not be disclosed
expect(filesUpload.thumbnails.length).toBe(thumbnails[i]);
for (const thumb of filesUpload.resource.thumbnails) {
for (const thumb of filesUpload.thumbnails) {
const thumbnails = await platform.app.inject({
headers: {"authorization": `Bearer ${await platform.auth.getJWTToken()}`},
method: "GET",
url: `${url}/companies/${platform.workspace.company_id}/files/${filesUpload.resource.id}/thumbnails/${thumb.index}`,
url: `${url}/companies/${platform.workspace.company_id}/files/${filesUpload.id}/thumbnails/${thumb.index}`,
});
expect(thumbnails.statusCode).toBe(200);
}
+6 -2
View File
@@ -13,6 +13,7 @@ import config from "config";
import globalResolver from "../../../src/services/global-resolver";
import {FileServiceImpl} from "../../../src/services/files/services";
import StorageAPI from "../../../src/core/platform/services/storage/provider";
import {SearchServiceAPI} from "../../../src/core/platform/services/search/api";
type TokenPayload = {
sub: string;
@@ -23,7 +24,7 @@ type TokenPayload = {
};
};
type User = {
export type User = {
id: string;
isWorkspaceModerator?: boolean;
};
@@ -42,6 +43,7 @@ export interface TestPlatform {
getJWTToken(payload?: TokenPayload): Promise<string>;
};
tearDown(): Promise<void>;
search: SearchServiceAPI;
}
export interface TestPlatformConfiguration {
@@ -76,6 +78,7 @@ export async function init(
const messageQueue = platform.getProvider<MessageQueueServiceAPI>("message-queue");
const auth = platform.getProvider<AuthServiceAPI>("auth");
const storage: StorageAPI = platform.getProvider<StorageAPI>("storage");
const search: SearchServiceAPI = platform.getProvider<SearchServiceAPI>("search");
testPlatform = {
platform,
@@ -91,6 +94,7 @@ export async function init(
getJWTToken,
},
tearDown,
search,
};
}
@@ -112,7 +116,7 @@ export async function init(
payload.sub = testPlatform.currentUser.id;
}
if (testPlatform .currentUser.isWorkspaceModerator) {
if (testPlatform.currentUser.isWorkspaceModerator) {
payload.org = {};
payload.org[testPlatform.workspace.company_id] = {
role: "",
@@ -6,6 +6,12 @@
"level": "warn"
}
},
"search": {
"type": "elasticsearch",
"elasticsearch": {
"endpoint": "http://localhost:9200"
}
},
"websocket": {
"path": "/socket",
"adapters": {