From 4ed7a4c857f5c96c50b4c0fab205d7435ac71ef4 Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Fri, 7 Apr 2023 09:23:58 +0100 Subject: [PATCH 1/8] fix: unit tests not running --- .../services/database/services/utils.test.ts | 31 ------------------- .../services/encryption/encryption.test.ts | 31 ------------------- 2 files changed, 62 deletions(-) delete mode 100644 twake/backend/node/test/unit/core/services/database/services/utils.test.ts delete mode 100644 twake/backend/node/test/unit/core/services/encryption/encryption.test.ts diff --git a/twake/backend/node/test/unit/core/services/database/services/utils.test.ts b/twake/backend/node/test/unit/core/services/database/services/utils.test.ts deleted file mode 100644 index 11fbac06..00000000 --- a/twake/backend/node/test/unit/core/services/database/services/utils.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import "reflect-metadata"; -import { describe, expect, it } from "@jest/globals"; -import { - fromMongoDbOrderable, - toMongoDbOrderable, -} from "../../../../../../src/core/platform/services/database/services/orm/utils"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import _ from "lodash"; -import { v1 as uuidv1 } from "uuid"; - -describe("The MongoDb to Orderable module", () => { - describe("The to/from orderable function", () => { - it("should be unique", () => { - const uuid1 = toMongoDbOrderable(uuidv1()); - const uuid2 = toMongoDbOrderable(uuidv1()); - const uuid3 = toMongoDbOrderable(uuidv1()); - const uuid4 = toMongoDbOrderable(uuidv1()); - - expect(_.uniq([uuid1, uuid2, uuid3, uuid4]).length).toBe(4); - }); - - it("should convert both ways", () => { - const uuid = uuidv1(); - const orderable = toMongoDbOrderable(uuid); - - expect(fromMongoDbOrderable(orderable)).toBe(uuid); - expect(orderable).toBe(toMongoDbOrderable(fromMongoDbOrderable(orderable))); - }); - }); -}); diff --git a/twake/backend/node/test/unit/core/services/encryption/encryption.test.ts b/twake/backend/node/test/unit/core/services/encryption/encryption.test.ts deleted file mode 100644 index 83f7ea1c..00000000 --- a/twake/backend/node/test/unit/core/services/encryption/encryption.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "@jest/globals"; -import { decrypt } from "../../../../../src/core/crypto/index"; -import v2 from "../../../../../src/core/crypto/v2"; -import v1 from "../../../../../src/core/crypto/v1"; -import legacy from "../../../../../src/core/crypto/legacy"; - -describe("Encryption", () => { - const encryptionKey = "a7c06651a7c063bb3e90c0c9a17eab88ab8977665127196a"; - - describe("The encrypt/decrypt functions", () => { - it("should successfully describe legacy encrypted values", async () => { - const legacyEncrypted = "encrypted_DwMLnKhuFbIanqBJPA5rcw=="; - - expect(legacy.decrypt(legacyEncrypted, encryptionKey).data).toBe("My company"); - expect(decrypt(legacyEncrypted, encryptionKey).data).toBe("My company"); - }); - - it("should successfully describe all versions", async () => { - const myData = { key: "some data" }; - - const v1Encrypted = v1.encrypt(myData, encryptionKey); - const v2Encrypted = v2.encrypt(myData, encryptionKey); - - expect(v1.decrypt(v1Encrypted.data, encryptionKey).data).toMatchObject(myData); - expect(v2.decrypt(v2Encrypted.data, encryptionKey).data).toMatchObject(myData); - - expect(decrypt(v1Encrypted.data, encryptionKey).data).toMatchObject(myData); - expect(decrypt(v2Encrypted.data, encryptionKey).data).toMatchObject(myData); - }); - }); -}); From 254df9828c35b0473097f5c93a659019d6aeedcd Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Fri, 7 Apr 2023 09:46:03 +0100 Subject: [PATCH 2/8] fix: ref e2e tests --- twake/backend/node/test/e2e/counters.spec.ts | 239 ------- .../node/test/e2e/documents/assets/test.txt | 1 - .../test/e2e/documents/documents-tab.spec.ts | 183 ----- .../node/test/e2e/documents/documents.spec.ts | 238 ------- .../backend/node/test/e2e/documents/utils.ts | 116 ---- .../backend/node/test/e2e/statistics.spec.ts | 155 ----- twake/backend/node/test/e2e/tags/tags.spec.ts | 308 --------- .../node/test/e2e/users/users.search.spec.ts | 149 ---- .../backend/node/test/e2e/users/users.spec.ts | 645 ------------------ .../node/test/e2e/websocket/auth.spec.ts | 90 --- .../node/test/e2e/websocket/realtime.spec.ts | 157 ----- 11 files changed, 2281 deletions(-) delete mode 100644 twake/backend/node/test/e2e/counters.spec.ts delete mode 100644 twake/backend/node/test/e2e/documents/assets/test.txt delete mode 100644 twake/backend/node/test/e2e/documents/documents-tab.spec.ts delete mode 100644 twake/backend/node/test/e2e/documents/documents.spec.ts delete mode 100644 twake/backend/node/test/e2e/documents/utils.ts delete mode 100644 twake/backend/node/test/e2e/statistics.spec.ts delete mode 100644 twake/backend/node/test/e2e/tags/tags.spec.ts delete mode 100644 twake/backend/node/test/e2e/users/users.search.spec.ts delete mode 100644 twake/backend/node/test/e2e/users/users.spec.ts delete mode 100644 twake/backend/node/test/e2e/websocket/auth.spec.ts delete mode 100644 twake/backend/node/test/e2e/websocket/realtime.spec.ts diff --git a/twake/backend/node/test/e2e/counters.spec.ts b/twake/backend/node/test/e2e/counters.spec.ts deleted file mode 100644 index d26a3cc5..00000000 --- a/twake/backend/node/test/e2e/counters.spec.ts +++ /dev/null @@ -1,239 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import { beforeAll, describe, expect, it } from "@jest/globals"; -import { init, TestPlatform } from "./setup"; -import { v1 as uuidv1 } from "uuid"; -import { CounterAPI } from "../../src/core/platform/services/counter/types"; -import { - WorkspaceCounterEntity, - WorkspaceCounterPrimaryKey, - WorkspaceCounterType, -} from "../../src/services/workspaces/entities/workspace_counters"; -import { CounterProvider } from "../../src/core/platform/services/counter/provider"; -import WorkspaceUser, { - getInstance as getWorkspaceUserInstance, - TYPE as WorkspaceUserEntityType, -} from "../../src/services/workspaces/entities/workspace_user"; - -import { countRepositoryItems } from "../../src/utils/counters"; -import { TestDbService } from "./utils.prepare.db"; -import { - ChannelCounterEntity, - ChannelCounterPrimaryKey, - ChannelUserCounterType, -} from "../../src/services/channels/entities/channel-counters"; -import { ChannelMemberType } from "../../src/services/channels/types"; -import { getMemberOfChannelInstance, MemberOfChannel } from "../../src/services/channels/entities"; -import gr from "../../src/services/global-resolver"; - -describe("Counters implementation", () => { - let platform: TestPlatform; - // let database: DatabaseServiceAPI; - let counterApi: CounterAPI; - let testDbService: TestDbService; - - beforeAll(async ends => { - platform = await init(); - - testDbService = new TestDbService(platform); - - await gr.database.getConnector().drop(); - - counterApi = platform.platform.getProvider("counter"); - expect(counterApi).toBeTruthy(); - - ends(); - }); - - afterAll(done => { - platform.tearDown().then(done); - }); - - const getCounter = async (type, entity) => { - return counterApi.getCounter(await testDbService.getRepository(type, entity)); - }; - - describe("Workspace counters", () => { - let counter: CounterProvider; - const counterPk = { id: uuidv1(), counter_type: WorkspaceCounterType.MEMBERS }; - - beforeAll(async ends => { - counter = await getCounter("workspace_counters", WorkspaceCounterEntity); - - const workspaceUserRepository = await testDbService.getRepository( - WorkspaceUserEntityType, - WorkspaceUser, - ); - - await workspaceUserRepository.save( - getWorkspaceUserInstance({ - workspaceId: counterPk.id, - userId: uuidv1(), - id: uuidv1(), - }), - ); - - expect(counter).toBeTruthy(); - - ends(); - }); - - it("Initializing empty value", async done => { - await counter.increase(counterPk, 0); - const val = await counter.get(counterPk); - expect(val).toEqual(0); - done(); - }); - - it("Adding value", async done => { - // adding 1 - - await counter.increase(counterPk, 1); - let val = await counter.get(counterPk); - expect(val).toEqual(1); - - // adding 2 - - await counter.increase(counterPk, 2); - val = await counter.get(counterPk); - expect(val).toEqual(3); - - done(); - }); - - it("Subtracting value", async done => { - // Subtracting 2 - - await counter.increase(counterPk, -2); - let val = await counter.get(counterPk); - expect(val).toEqual(1); - - // Subtracting 10 - - await counter.increase(counterPk, -10); - val = await counter.get(counterPk); - expect(val).toEqual(-9); - - done(); - }); - - it("Revising counter", async done => { - // Subtracting 2 - - const workspaceUserRepository = await testDbService.getRepository( - WorkspaceUserEntityType, - WorkspaceUser, - ); - - counter.setReviseCallback(async (pk: WorkspaceCounterPrimaryKey) => { - if (pk.counter_type == "members") { - return countRepositoryItems(workspaceUserRepository, { workspace_id: pk.id }); - } - }, 4); - - await counter.increase(counterPk, 1); - const val = await counter.get(counterPk); - expect(val).toEqual(1); - - done(); - }); - }); - - describe("Channel counters", () => { - let counter: CounterProvider; - let counterPk: ChannelCounterPrimaryKey; - - beforeAll(async ends => { - counterPk = { - id: uuidv1(), - company_id: uuidv1(), - workspace_id: uuidv1(), - counter_type: ChannelUserCounterType.MEMBERS, - }; - - counter = await getCounter("channel_counters", ChannelCounterEntity); - - const memberOfChannelRepository = await testDbService.getRepository( - "channel_members", - MemberOfChannel, - ); - - await memberOfChannelRepository.save( - getMemberOfChannelInstance({ - company_id: counterPk.company_id, - workspace_id: counterPk.workspace_id, - channel_id: counterPk.id, - user_id: uuidv1(), - }), - ); - - expect(counter).toBeTruthy(); - - ends(); - }); - - it("Initializing empty value", async done => { - await counter.increase(counterPk, 0); - const val = await counter.get(counterPk); - expect(val).toEqual(0); - done(); - }); - - it("Adding value", async done => { - // adding 1 - - await counter.increase(counterPk, 1); - let val = await counter.get(counterPk); - expect(val).toEqual(1); - - // adding 2 - - await counter.increase(counterPk, 2); - val = await counter.get(counterPk); - expect(val).toEqual(3); - - done(); - }); - - it("Subtracting value", async done => { - // Subtracting 2 - - await counter.increase(counterPk, -2); - let val = await counter.get(counterPk); - expect(val).toEqual(1); - - // Subtracting 10 - - await counter.increase(counterPk, -10); - val = await counter.get(counterPk); - expect(val).toEqual(-9); - - done(); - }); - - it("Revising counter", async done => { - // Subtracting 2 - - const memberOfChannelRepository = await testDbService.getRepository( - "channel_members", - MemberOfChannel, - ); - - counter.setReviseCallback(async (pk: ChannelCounterPrimaryKey) => { - if (pk.counter_type == ChannelUserCounterType.MEMBERS) { - return countRepositoryItems( - memberOfChannelRepository, - { channel_id: pk.id, company_id: pk.company_id, workspace_id: pk.workspace_id }, - { type: ChannelMemberType.MEMBER }, - ); - } - }, 4); - - await counter.increase(counterPk, 1); - const val = await counter.get(counterPk); - expect(val).toEqual(1); - - done(); - }); - }); -}); diff --git a/twake/backend/node/test/e2e/documents/assets/test.txt b/twake/backend/node/test/e2e/documents/assets/test.txt deleted file mode 100644 index 9daeafb9..00000000 --- a/twake/backend/node/test/e2e/documents/assets/test.txt +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/twake/backend/node/test/e2e/documents/documents-tab.spec.ts b/twake/backend/node/test/e2e/documents/documents-tab.spec.ts deleted file mode 100644 index e9d6c84e..00000000 --- a/twake/backend/node/test/e2e/documents/documents-tab.spec.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { afterAll, afterEach, beforeEach, describe, expect, it } from "@jest/globals"; -import { deserialize } from "class-transformer"; -import { AccessInformation } from "../../../src/services/documents/entities/drive-file"; -import { init, TestPlatform } from "../setup"; -import { TestDbService } from "../utils.prepare.db"; -import { e2e_createDocument, e2e_getDocument } from "./utils"; - -const url = "/internal/services/documents/v1"; - -describe("the Drive Twake tabs feature", () => { - let platform: TestPlatform; - - class DriveFileMockClass { - id: string; - name: string; - size: number; - added: string; - parent_id: string; - access_info: AccessInformation; - } - - class DriveItemDetailsMockClass { - path: string[]; - item: DriveFileMockClass; - children: DriveFileMockClass[]; - versions: Record[]; - } - - beforeEach(async () => { - platform = await init({ - services: [ - "webserver", - "database", - "applications", - "search", - "storage", - "message-queue", - "user", - "search", - "files", - "websocket", - "messages", - "auth", - "realtime", - "channels", - "counter", - "statistics", - "platform-services", - "documents", - ], - }); - }); - - afterEach(async () => { - await platform.tearDown(); - }); - - afterAll(async () => { - await platform.app.close(); - }); - - it("did create a tab configuration on Drive side", async done => { - await TestDbService.getInstance(platform, true); - - const item = { - name: "new tab test file", - parent_id: "root", - company_id: platform.workspace.company_id, - }; - - const version = {}; - - const response = await e2e_createDocument(platform, item, version); - const doc = deserialize(DriveFileMockClass, response.body); - - const tab = { - company_id: platform.workspace.company_id, - tab_id: "1234567890", - channel_id: "abcdefghij", - item_id: doc.id, - level: "write", - }; - - const token = await platform.auth.getJWTToken(); - - const createdTab = await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/tabs/${tab.tab_id}`, - headers: { - authorization: `Bearer ${token}`, - }, - payload: tab, - }); - - expect(createdTab.statusCode).toBe(200); - expect(createdTab.body).toBeDefined(); - expect(createdTab.json().company_id).toBe(tab.company_id); - expect(createdTab.json().tab_id).toBe(tab.tab_id); - expect(createdTab.json().item_id).toBe(tab.item_id); - - const getTabResponse = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/tabs/${tab.tab_id}`, - headers: { - authorization: `Bearer ${token}`, - }, - }); - - expect(getTabResponse.statusCode).toBe(200); - expect(getTabResponse.body).toBeDefined(); - expect(getTabResponse.json().company_id).toBe(tab.company_id); - expect(getTabResponse.json().tab_id).toBe(tab.tab_id); - expect(getTabResponse.json().item_id).toBe(tab.item_id); - - const documentResponse = await e2e_getDocument(platform, doc.id); - const documentResult = deserialize( - DriveItemDetailsMockClass, - documentResponse.body, - ); - - console.log(documentResult?.item); - - expect( - documentResult?.item?.access_info?.entities?.find( - a => a?.type === "channel" && a.id === "abcdefghij" && a.level === "write", - ), - ).toBeDefined(); - - done?.(); - }); - - it("did refuse to create a tab configuration for an item I can't manage", async done => { - const dbService = await TestDbService.getInstance(platform, true); - const ws0pk = { - id: platform.workspace.workspace_id, - company_id: platform.workspace.company_id, - }; - const otherUser = await dbService.createUser([ws0pk]); - - const item = { - name: "new tab test file", - parent_id: "root", - company_id: platform.workspace.company_id, - access_info: { - entities: [ - { - type: "folder", - id: "parent", - level: "none", - } as any, - ], - }, - }; - - const version = {}; - - const response = await e2e_createDocument(platform, item, version); - const doc = deserialize(DriveFileMockClass, response.body); - - const tab = { - company_id: platform.workspace.company_id, - tab_id: "1234567890", - channel_id: "abcdefghij", - item_id: doc.id, - level: "read", - }; - - const token = await platform.auth.getJWTToken({ sub: otherUser.id }); - - const createdTab = await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/tabs/${tab.tab_id}`, - headers: { - authorization: `Bearer ${token}`, - }, - payload: tab, - }); - - expect(createdTab.statusCode).toBe(403); - - done?.(); - }); -}); diff --git a/twake/backend/node/test/e2e/documents/documents.spec.ts b/twake/backend/node/test/e2e/documents/documents.spec.ts deleted file mode 100644 index d964740e..00000000 --- a/twake/backend/node/test/e2e/documents/documents.spec.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { describe, beforeEach, afterEach, it, expect, afterAll } from "@jest/globals"; -import { deserialize } from "class-transformer"; -import { File } from "../../../src/services/files/entities/file"; -import { ResourceUpdateResponse } from "../../../src/utils/types"; -import { init, TestPlatform } from "../setup"; -import { TestDbService } from "../utils.prepare.db"; -import { - e2e_createDocument, - e2e_createDocumentFile, - e2e_createVersion, - e2e_deleteDocument, - e2e_getDocument, - e2e_searchDocument, - e2e_updateDocument, -} from "./utils"; - -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[]; - } - - class SearchResultMockClass { - entities: DriveFileMockClass[]; - } - - beforeEach(async () => { - platform = await init({ - services: [ - "webserver", - "database", - "applications", - "search", - "storage", - "message-queue", - "user", - "search", - "files", - "websocket", - "messages", - "auth", - "realtime", - "channels", - "counter", - "statistics", - "platform-services", - "documents", - ], - }); - }); - - afterEach(async () => { - await platform.tearDown(); - }); - - afterAll(async () => { - await platform.app.close(); - }); - - const createItem = async (): Promise => { - await TestDbService.getInstance(platform, true); - - const item = { - name: "new test file", - parent_id: "root", - company_id: platform.workspace.company_id, - }; - - const version = {}; - - const response = await e2e_createDocument(platform, item, version); - return deserialize(DriveFileMockClass, response.body); - }; - - it("did create the drive item", async done => { - const result = await createItem(); - - expect(result).toBeDefined(); - expect(result.name).toEqual("new test file"); - expect(result.added).toBeDefined(); - - done?.(); - }); - - it("did fetch the drive item", async done => { - await TestDbService.getInstance(platform, true); - - const response = await e2e_getDocument(platform, ""); - const result = deserialize(DriveItemDetailsMockClass, response.body); - - expect(result.item.name).toEqual("root"); - - done?.(); - }); - - it("did fetch the trash", async done => { - await TestDbService.getInstance(platform, true); - - const response = await e2e_getDocument(platform, "trash"); - const result = deserialize(DriveItemDetailsMockClass, response.body); - - expect(result.item.name).toEqual("trash"); - - done?.(); - }); - - it("did delete an item", async done => { - const createItemResult = await createItem(); - - expect(createItemResult.id).toBeDefined(); - - const deleteResponse = await e2e_deleteDocument(platform, createItemResult.id); - expect(deleteResponse.statusCode).toEqual(200); - - done?.(); - }); - - it("did update an item", async done => { - const createItemResult = await createItem(); - - expect(createItemResult.id).toBeDefined(); - - const update = { - name: "somethingelse", - }; - - const updateItemResponse = await e2e_updateDocument(platform, createItemResult.id, update); - const updateItemResult = deserialize( - DriveFileMockClass, - updateItemResponse.body, - ); - - expect(createItemResult.id).toEqual(updateItemResult.id); - expect(updateItemResult.name).toEqual("somethingelse"); - - done?.(); - }); - - it("did move an item to trash", async done => { - const createItemResult = await createItem(); - - expect(createItemResult.id).toBeDefined(); - - const moveToTrashResponse = await e2e_deleteDocument(platform, createItemResult.id); - expect(moveToTrashResponse.statusCode).toEqual(200); - - const listTrashResponse = await e2e_getDocument(platform, "trash"); - const listTrashResult = deserialize( - DriveItemDetailsMockClass, - listTrashResponse.body, - ); - - expect(listTrashResult.item.name).toEqual("trash"); - expect(listTrashResult.children.some(({ id }) => id === createItemResult.id)).toBeTruthy(); - - done?.(); - }); - - // TODO: wait for elastic search index - it("did search for an item", async done => { - const createItemResult = await createItem(); - - expect(createItemResult.id).toBeDefined(); - - await e2e_getDocument(platform, "root"); - await e2e_getDocument(platform, createItemResult.id); - - await new Promise(resolve => setTimeout(resolve, 3000)); - - const searchPayload = { - search: "test", - }; - - const searchResponse = await e2e_searchDocument(platform, searchPayload); - const searchResult = deserialize( - SearchResultMockClass, - searchResponse.body, - ); - - expect(searchResult.entities.length).toBeGreaterThanOrEqual(1); - - done?.(); - }); - - it("did search for an item that doesn't exist", async done => { - await createItem(); - - const unexistingSeachPayload = { - search: "somethingthatdoesn'tandshouldn'texist", - }; - const failSearchResponse = await e2e_searchDocument(platform, unexistingSeachPayload); - const failSearchResult = deserialize( - SearchResultMockClass, - failSearchResponse.body, - ); - - expect(failSearchResult.entities).toHaveLength(0); - - done?.(); - }); - - it("did create a version for a drive item", async done => { - const item = await createItem(); - const fileUploadResponse = await e2e_createDocumentFile(platform); - const fileUploadResult = deserialize>( - ResourceUpdateResponse, - fileUploadResponse.body, - ); - - const file_metadata = { external_id: fileUploadResult.resource.id }; - - await e2e_createVersion(platform, item.id, { filename: "file2", file_metadata }); - await e2e_createVersion(platform, item.id, { filename: "file3", file_metadata }); - await e2e_createVersion(platform, item.id, { filename: "file4", file_metadata }); - - const fetchItemResponse = await e2e_getDocument(platform, item.id); - const fetchItemResult = deserialize( - DriveItemDetailsMockClass, - fetchItemResponse.body, - ); - - expect(fetchItemResult.versions).toHaveLength(4); - - done?.(); - }); -}); diff --git a/twake/backend/node/test/e2e/documents/utils.ts b/twake/backend/node/test/e2e/documents/utils.ts deleted file mode 100644 index 1104441c..00000000 --- a/twake/backend/node/test/e2e/documents/utils.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { DriveFile } from "../../../src/services/documents/entities/drive-file"; -import { FileVersion } from "../../../src/services/documents/entities/file-version"; -import { TestPlatform } from "../setup"; -import formAutoContent from "form-auto-content"; -import fs from "fs"; - -const url = "/internal/services/documents/v1"; - -export const e2e_createDocument = async ( - platform: TestPlatform, - item: Partial, - version: Partial, -) => { - const token = await platform.auth.getJWTToken(); - - return await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/item`, - headers: { - authorization: `Bearer ${token}`, - }, - payload: { - item, - version, - }, - }); -}; - -export const e2e_getDocument = async (platform: TestPlatform, id: string | "root" | "trash") => { - const token = await platform.auth.getJWTToken(); - - return await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/item/${id}`, - headers: { - authorization: `Bearer ${token}`, - }, - }); -}; - -export const e2e_deleteDocument = async (platform: TestPlatform, id: string | "root" | "trash") => { - const token = await platform.auth.getJWTToken(); - - return await platform.app.inject({ - method: "DELETE", - url: `${url}/companies/${platform.workspace.company_id}/item/${id}`, - headers: { - authorization: `Bearer ${token}`, - }, - }); -}; - -export const e2e_updateDocument = async ( - platform: TestPlatform, - id: string | "root" | "trash", - item: Partial, -) => { - const token = await platform.auth.getJWTToken(); - - return await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/item/${id}`, - headers: { - authorization: `Bearer ${token}`, - }, - payload: item, - }); -}; - -export const e2e_searchDocument = async ( - platform: TestPlatform, - payload: Record, -) => { - const token = await platform.auth.getJWTToken(); - - const response = await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/search`, - headers: { - authorization: `Bearer ${token}`, - }, - payload, - }); - - return response; -}; - -export const e2e_createVersion = async ( - platform: TestPlatform, - id: string, - payload: Partial, -) => { - const token = await platform.auth.getJWTToken(); - - return await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/item/${id}/version`, - headers: { - authorization: `Bearer ${token}`, - }, - payload, - }); -}; - -export const e2e_createDocumentFile = async (platform: TestPlatform) => { - const filePath = `${__dirname}/assets/test.txt`; - const token = await platform.auth.getJWTToken(); - const form = formAutoContent({ file: fs.createReadStream(filePath) }); - form.headers["authorization"] = `Bearer ${token}`; - - return await platform.app.inject({ - method: "POST", - url: `/internal/services/files/v1/companies/${platform.workspace.company_id}/files`, - ...form, - }); -}; diff --git a/twake/backend/node/test/e2e/statistics.spec.ts b/twake/backend/node/test/e2e/statistics.spec.ts deleted file mode 100644 index 4d70b35f..00000000 --- a/twake/backend/node/test/e2e/statistics.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from "@jest/globals"; -import { init, TestPlatform } from "./setup"; - -import { v1 as uuidv1 } from "uuid"; -import { - createMessage, - createParticipant, - e2e_createMessage, - e2e_createThread, -} from "./messages/utils"; -import { Thread } from "../../src/services/messages/entities/threads"; -import { deserialize } from "class-transformer"; -import { WorkspaceExecutionContext } from "../../src/services/channels/types"; -import { ChannelUtils, get as getChannelUtils } from "./channels/utils"; -import { ResourceUpdateResponse, User } from "../../src/utils/types"; -import gr from "../../src/services/global-resolver"; - -describe("Statistics implementation", () => { - let platform: TestPlatform; - // let database: DatabaseServiceAPI; - let channelUtils: ChannelUtils; - - beforeAll(async ends => { - platform = await init({ - services: ["database", "statistics", "webserver", "auth"], - }); - - expect(gr.services.statistics).toBeTruthy(); - - ends(); - channelUtils = getChannelUtils(platform); - }); - - beforeEach(async ends => { - await platform.database.getConnector().drop(); - ends(); - }); - - afterAll(async done => { - await platform.tearDown(); - done(); - }); - - it("Check statistics counters", async done => { - console.log(await gr.services.statistics.get(undefined, "counter-test")); - - expect(await gr.services.statistics.get(undefined, "counter-test")).toEqual(0); - - await gr.services.statistics.increase(platform.workspace.company_id, "counter-test"); - await gr.services.statistics.increase(platform.workspace.company_id, "counter-test"); - const secondCompanyId = uuidv1(); - await gr.services.statistics.increase(secondCompanyId, "counter-test"); - await gr.services.statistics.increase(secondCompanyId, "counter-test"); - await gr.services.statistics.increase(platform.workspace.company_id, "counter-test2"); - - expect(await gr.services.statistics.get(platform.workspace.company_id, "counter-test")).toEqual( - 2, - ); - expect(await gr.services.statistics.get(secondCompanyId, "counter-test")).toEqual(2); - expect(await gr.services.statistics.get(undefined, "counter-test")).toEqual(4); - - expect( - await gr.services.statistics.get(platform.workspace.company_id, "counter-test2"), - ).toEqual(1); - expect(await gr.services.statistics.get(secondCompanyId, "counter-test2")).toEqual(0); - expect(await gr.services.statistics.get(undefined, "counter-test2")).toEqual(1); - - done(); - }); - - function getContext(user?: User): WorkspaceExecutionContext { - return { - workspace: platform.workspace, - user: user || platform.currentUser, - }; - } - - async function sleep(timeout = 0) { - return new Promise(r => setTimeout(r, timeout)); - } - - describe("On user use messages in channel view", () => { - it("should create a message and retrieve it in channel view", async () => { - const channel = channelUtils.getChannel(); - await gr.services.channels.channels.save(channel, {}, getContext()); - const channelId = channel.id; - - //Reset global value because messages could have been created somewhere else - const value = await gr.services.statistics.get(undefined, "messages"); - await gr.services.statistics.increase(undefined, "messages", -value); - - const response = await e2e_createThread( - platform, - [ - createParticipant( - { - type: "channel", - id: channelId, - }, - platform, - ), - ], - createMessage({ text: "Initial thread 1 message" }), - ); - - await sleep(); - - const result: ResourceUpdateResponse = deserialize( - ResourceUpdateResponse, - response.body, - ); - const threadId = result.resource.id; - - await e2e_createMessage(platform, threadId, createMessage({ text: "Reply 1" })); - - await e2e_createMessage(platform, threadId, createMessage({ text: "Reply 2" })); - await e2e_createThread( - platform, - [ - createParticipant( - { - type: "channel", - id: channelId, - }, - platform, - ), - ], - createMessage({ text: "Initial thread 2 message" }), - ); - - await e2e_createMessage(platform, threadId, createMessage({ text: "Reply 3" })); - - await e2e_createThread( - platform, - [ - createParticipant( - { - type: "channel", - id: channelId, - }, - platform, - ), - ], - createMessage({ text: "Initial thread 3 message" }), - ); - - await new Promise(r => setTimeout(r, 5000)); - - expect(await gr.services.statistics.get(undefined, "messages")).toEqual(6); - expect(await gr.services.statistics.get(platform.workspace.company_id, "messages")).toEqual( - 6, - ); - }); - }); -}); diff --git a/twake/backend/node/test/e2e/tags/tags.spec.ts b/twake/backend/node/test/e2e/tags/tags.spec.ts deleted file mode 100644 index bdb6129d..00000000 --- a/twake/backend/node/test/e2e/tags/tags.spec.ts +++ /dev/null @@ -1,308 +0,0 @@ -import "reflect-metadata"; -import { afterAll, beforeAll, describe, expect, it } from "@jest/globals"; -import { init, TestPlatform } from "../setup"; -import { TestDbService } from "../utils.prepare.db"; -import { Tags } from "../../../src/services/tags/entities/tags"; -import { deserialize } from "class-transformer"; -import { - ResourceCreateResponse, - ResourceGetResponse, - ResourceListResponse, -} from "../../../src/utils/types"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore - -describe("The Tags feature", () => { - const url = "/internal/services/tags/v1"; - let platform: TestPlatform; - let testDbService: TestDbService; - const tagIds: string[] = []; - - beforeAll(async ends => { - platform = await init({ - services: ["webserver", "database", "storage", "message-queue", "tags"], - }); - testDbService = await TestDbService.getInstance(platform, true); - ends(); - }); - - afterAll(async done => { - for (let i = 0; i < tagIds.length; i++) { - for (let j = 0; j < tagIds.length; j++) { - if (tagIds[j] === tagIds[i] && j !== i) { - throw new Error("Tag are not unique"); - } - } - } - await platform?.tearDown(); - platform = null; - done(); - }); - - describe("Create tag", () => { - it("should 201 if creator is a company admin", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "admin", - }); - - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - - for (let i = 0; i < 3; i++) { - const createTag = await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/tags`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - payload: { - name: `test${i}`, - colour: `#00000${i}`, - }, - }); - - const tagResult: ResourceCreateResponse = deserialize( - ResourceCreateResponse, - createTag.body, - ); - expect(createTag.statusCode).toBe(201); - expect(tagResult.resource).toBeDefined(); - expect(tagResult.resource.name).toBe(`test${i}`); - expect(tagResult.resource.colour).toBe(`#00000${i}`); - expect(tagResult.resource.company_id).toBe(platform.workspace.company_id); - expect(tagResult.resource.tag_id).toBe(`test${i}`); - - const getTag = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/tags/${tagResult.resource.tag_id}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(getTag.statusCode).toBe(200); - - tagIds.push(tagResult.resource.tag_id); - } - done(); - }); - - it("should 401 if creator is not a company admin", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "member", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const createTag = await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/tags`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - payload: { - name: "testNotAdmin", - colour: "#000000", - }, - }); - expect(createTag.statusCode).toBe(401); - - const tagResult: ResourceCreateResponse = deserialize( - ResourceCreateResponse, - createTag.body, - ); - expect(tagResult.resource).toBe(undefined); - done(); - }); - }); - - describe("Get tag", () => { - it("should 200 get a tag", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "member", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const getTag = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/tags/${tagIds[0]}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(getTag.statusCode).toBe(200); - - const getResult: ResourceGetResponse = deserialize(ResourceGetResponse, getTag.body); - expect(getResult.resource).toBeDefined(); - expect(getResult.resource.name).toBe("test0"); - expect(getResult.resource.colour).toBe("#000000"); - expect(getResult.resource.company_id).toBe(platform.workspace.company_id); - - done(); - }); - - it("should 200 tag does not exist", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "member", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const getTag = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/tags/NonExistingTag`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(getTag.statusCode).toBe(200); - - const getResult: ResourceGetResponse = deserialize(ResourceGetResponse, getTag.body); - expect(getResult.resource).toBe(null); - - done(); - }); - }); - - describe("Update tag", () => { - it("Should 204 if user is admin", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "admin", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const updateTag = await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/tags`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - payload: { - name: "test1", - colour: "#000003", - }, - }); - expect(updateTag.statusCode).toBe(201); - - const tagUpdatedResult: ResourceCreateResponse = deserialize( - ResourceCreateResponse, - updateTag.body, - ); - expect(tagUpdatedResult.resource).toBeDefined(); - expect(tagUpdatedResult.resource.name).toBe("test1"); - expect(tagUpdatedResult.resource.colour).toBe("#000003"); - expect(tagUpdatedResult.resource.company_id).toBe(platform.workspace.company_id); - expect(tagUpdatedResult.resource.tag_id).toBe("test1"); - - const getUpdatedTag = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/tags/${tagUpdatedResult.resource.tag_id}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(getUpdatedTag.statusCode).toBe(200); - - done(); - }); - - it("should 401 if creator is not a company admin", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "member", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const createTag = await platform.app.inject({ - method: "POST", - url: `${url}/companies/${platform.workspace.company_id}/tags/testNotAdmin`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - payload: { - name: "testNotAdmin", - colour: "#000000", - }, - }); - expect(createTag.statusCode).toBe(401); - - const tagResult: ResourceCreateResponse = deserialize( - ResourceCreateResponse, - createTag.body, - ); - console.log("tagResult2", tagResult, jwtToken); - expect(tagResult.resource).toBe(undefined); - - done(); - }); - }); - - describe("List tags", () => { - it("should 200 list a tag", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "member", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const listTag = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/tags`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(listTag.statusCode).toBe(200); - - const tagResult: ResourceListResponse = deserialize(ResourceListResponse, listTag.body); - expect(tagResult.resources).toBeDefined(); - for (const tag of tagResult.resources) { - expect(tag.name).toBeDefined(); - expect(tag.colour).toBeDefined(); - expect(tag.company_id).toBe(platform.workspace.company_id); - expect(tag.tag_id).toBeDefined(); - } - - done(); - }); - }); - - describe("Delete tag", () => { - it("should 200 if admin delete a tag", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "admin", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const deleteTag = await platform.app.inject({ - method: "DELETE", - url: `${url}/companies/${platform.workspace.company_id}/tags/${tagIds[0]}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(deleteTag.statusCode).toBe(200); - - done(); - }); - - it("should 200 if tag does not exist", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "admin", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const deleteTag = await platform.app.inject({ - method: "DELETE", - url: `${url}/companies/${platform.workspace.company_id}/tags/NonExistingTag`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(deleteTag.statusCode).toBe(200); - done(); - }); - - it("should 401 if not admin", async done => { - const user = await testDbService.createUser([testDbService.defaultWorkspace()], { - companyRole: "member", - }); - const jwtToken = await platform.auth.getJWTToken({ sub: user.id }); - const deleteTag = await platform.app.inject({ - method: "DELETE", - url: `${url}/companies/${platform.workspace.company_id}/tags/${tagIds[0]}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(deleteTag.statusCode).toBe(401); - done(); - }); - }); -}); diff --git a/twake/backend/node/test/e2e/users/users.search.spec.ts b/twake/backend/node/test/e2e/users/users.search.spec.ts deleted file mode 100644 index b95d1320..00000000 --- a/twake/backend/node/test/e2e/users/users.search.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; -import { init, TestPlatform } from "../setup"; -import { TestDbService } from "../utils.prepare.db"; -import { v1 as uuidv1 } from "uuid"; - -describe("The /users API", () => { - const url = "/internal/services/users/v1"; - let platform: TestPlatform; - - beforeEach(async ends => { - platform = await init({ - services: [ - "database", - "search", - "message-queue", - "websocket", - "webserver", - "user", - "auth", - "applications", - "storage", - "counter", - "workspaces", - "console", - "statistics", - "platform-services", - ], - }); - ends(); - }); - - afterEach(async ends => { - platform && (await platform.tearDown()); - platform = null; - ends(); - }); - - describe("The GET /users/?search=... route", () => { - it("Should find the searched users", async done => { - const testDbService = new TestDbService(platform); - await testDbService.createCompany(platform.workspace.company_id); - const workspacePk = { - id: platform.workspace.workspace_id, - company_id: platform.workspace.company_id, - }; - const workspacePk2 = { - id: uuidv1(), - company_id: uuidv1(), - }; - await testDbService.createWorkspace(workspacePk); - await testDbService.createWorkspace(workspacePk2); - await testDbService.createUser([workspacePk], { - firstName: "Ha", - lastName: "Nguyen", - email: "hnguyen@twake.app", - }); - await testDbService.createUser([workspacePk], { - firstName: "Harold", - lastName: "Georges", - email: "hgeorges@twake.app", - }); - await testDbService.createUser([workspacePk], { - firstName: "Bob", - lastName: "Smith", - email: "bob@twake.app", - }); - await testDbService.createUser([workspacePk], { - firstName: "Bob", - lastName: "Rabiot", - email: "rabiot.b@twake.app", - }); - await testDbService.createUser([workspacePk, workspacePk2], { - firstName: "Bob", - lastName: "Smith-Rabiot", - email: "rbs@twake.app", - }); - await testDbService.createUser([workspacePk], { - firstName: "Alexïs", - lastName: "Goélâns", - email: "alexis.goelans@twake.app", - }); - - //Wait for indexation to happen - await new Promise(r => setTimeout(r, 5000)); - - let resources = await search("ha"); - expect(resources.length).toBe(2); - - resources = await search("bob rabiot"); - - expect(resources.map(e => e.email).includes("rabiot.b@twake.app")).toBe(true); - expect(resources.map(e => e.email).includes("rbs@twake.app")).toBe(true); - expect(resources.map(e => e.email).includes("bob@twake.app")).toBe(true); - - resources = await search("alexis"); - expect(resources[0].email).toBe("alexis.goelans@twake.app"); - - resources = await search("ALEXIS"); - expect(resources[0].email).toBe("alexis.goelans@twake.app"); - - resources = await search("AleXis"); - expect(resources[0].email).toBe("alexis.goelans@twake.app"); - - resources = await search("alex"); - expect(resources[0].email).toBe("alexis.goelans@twake.app"); - - resources = await search("àlèXïs"); - expect(resources[0].email).toBe("alexis.goelans@twake.app"); - - resources = await search("rbs"); - expect(resources[0].email).toBe("rbs@twake.app"); - - resources = await search("rbs@twake.app"); - expect(resources[0].email).toBe("rbs@twake.app"); - - resources = await search("bob", workspacePk2.company_id); - expect(resources.length).toBe(1); - - resources = await search("rbs@twake.app", workspacePk.company_id); - expect(resources[0].email).toBe("rbs@twake.app"); - - resources = await search("rbs@twake.app", uuidv1()); - expect(resources.length).toBe(0); - - done(); - }); - }); - - async function search(search: string, companyId?: string): Promise { - const jwtToken = await platform.auth.getJWTToken(); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - query: { - search: search, - ...(companyId ? { search_company_id: companyId } : {}), - }, - }); - - expect(response.statusCode).toBe(200); - const json = response.json(); - expect(json).toMatchObject({ resources: expect.any(Array) }); - const resources = json.resources; - return resources; - } -}); diff --git a/twake/backend/node/test/e2e/users/users.spec.ts b/twake/backend/node/test/e2e/users/users.spec.ts deleted file mode 100644 index a8c03130..00000000 --- a/twake/backend/node/test/e2e/users/users.spec.ts +++ /dev/null @@ -1,645 +0,0 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "@jest/globals"; -import { init, TestPlatform } from "../setup"; -import { TestDbService } from "../utils.prepare.db"; -import { v1 as uuidv1 } from "uuid"; -import { CompanyLimitsEnum, UserObject } from "../../../src/services/user/web/types"; -import { ChannelVisibility } from "../../../src/services/channels/types"; -import gr from "../../../src/services/global-resolver"; -import { ResourceListResponse, Workspace } from "../../../src/utils/types"; -import { ChannelSaveOptions } from "../../../src/services/channels/web/types"; -import { createMessage, e2e_createThread } from "../messages/utils"; -import { deserialize } from "class-transformer"; -import { get as getChannelUtils } from "../channels/utils"; - -describe("The /users API", () => { - const url = "/internal/services/users/v1"; - let platform: TestPlatform; - - let testDbService: TestDbService; - - const nonExistentId = uuidv1(); - - beforeEach(async ends => { - platform = await init(); - ends(); - }); - afterEach(async ends => { - await platform.tearDown(); - platform = null; - ends(); - }); - - beforeAll(async ends => { - const platform = await init({ - services: [ - "database", - "search", - "message-queue", - "websocket", - "applications", - "webserver", - "user", - "auth", - "storage", - "counter", - "console", - "workspaces", - "statistics", - "platform-services", - ], - }); - - testDbService = await TestDbService.getInstance(platform); - await testDbService.createCompany(); - const workspacePk = { id: uuidv1(), company_id: testDbService.company.id }; - await testDbService.createWorkspace(workspacePk); - await testDbService.createUser([workspacePk], { - workspaceRole: "moderator", - companyRole: "admin", - email: "admin@admin.admin", - username: "adminuser", - firstName: "admin", - }); - await testDbService.createUser([workspacePk]); - - ends(); - }); - - afterAll(async ends => { - ends(); - }); - - describe("The GET /users/:id route", () => { - it("should 401 when not authenticated", async done => { - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users/1`, - }); - - expect(response.statusCode).toBe(401); - done(); - }); - - it("should 404 when user does not exists", async done => { - const jwtToken = await platform.auth.getJWTToken({ sub: testDbService.users[0].id }); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users/${nonExistentId}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - expect(response.statusCode).toBe(404); - expect(response.json()).toEqual({ - error: "Not Found", - message: `User ${nonExistentId} not found`, - statusCode: 404, - }); - done(); - }); - - it("should 200 and big response for myself", async done => { - const myId = testDbService.users[0].id; - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users/${myId}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - expect(response.statusCode).toBe(200); - - const resource = response.json()["resource"]; - - expect(resource).toMatchObject({ - id: myId, - provider: expect.any(String), - provider_id: expect.any(String), - email: expect.any(String), - is_verified: expect.any(Boolean), - picture: expect.any(String), - first_name: expect.any(String), - last_name: expect.any(String), - created_at: expect.any(Number), - deleted: expect.any(Boolean), - status: expect.any(String), - last_activity: expect.any(Number), - - //Below is only if this is myself - - preference: expect.objectContaining({ - locale: expect.any(String), - timezone: expect.any(Number), - }), - }); - - expect(resource["companies"]).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - role: expect.stringMatching(/owner|admin|member|guest/), - status: expect.stringMatching(/active|deactivated|invited/), - company: { - id: expect.any(String), - name: expect.any(String), - logo: expect.any(String), - }, - }), - ]), - ); - - done(); - }); - - it("should 200 and short response for another user", async done => { - const myId = testDbService.users[0].id; - const anotherUserId = testDbService.users[1].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users/${anotherUserId}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - expect(response.statusCode).toBe(200); - - const resource = response.json()["resource"]; - - expect(resource).toMatchObject({ - id: anotherUserId, - provider: expect.any(String), - provider_id: expect.any(String), - email: expect.any(String), - is_verified: expect.any(Boolean), - picture: expect.any(String), - first_name: expect.any(String), - last_name: expect.any(String), - created_at: expect.any(Number), - deleted: expect.any(Boolean), - status: expect.any(String), - last_activity: expect.any(Number), - }); - - expect(resource).not.toMatchObject({ - locale: expect.anything(), - timezone: expect.anything(), - companies: expect.anything(), - }); - - done(); - }); - }); - - describe("The GET /users route", () => { - it("should 401 when user is not authenticated", async done => { - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users`, - }); - - expect(response.statusCode).toBe(401); - done(); - }); - - it("should 200 with array of users", async done => { - const myId = testDbService.users[0].id; - const anotherUserId = testDbService.users[1].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - query: { - user_ids: `${myId},${anotherUserId}`, - company_ids: "fd96c8a8-ae77-11eb-a1a1-0242ac120005", - }, - }); - - expect(response.statusCode).toBe(200); - const json = response.json(); - expect(json).toMatchObject({ resources: expect.any(Array) }); - const resources = json.resources; - - done(); - }); - }); - - describe("The GET /users/:user_id/companies route", () => { - it("should 401 when not authenticated", async done => { - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users/1/companies`, - }); - - expect(response.statusCode).toBe(401); - done(); - }); - - it("should 404 when user does not exists", async done => { - const jwtToken = await platform.auth.getJWTToken({ sub: testDbService.users[0].id }); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users/${nonExistentId}/companies`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - expect(response.statusCode).toBe(404); - expect(response.json()).toEqual({ - error: "Not Found", - message: `User ${nonExistentId} not found`, - statusCode: 404, - }); - done(); - }); - - it("should 200 and on correct request", async done => { - const myId = testDbService.users[0].id; - const anotherUserId = testDbService.users[1].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/users/${anotherUserId}/companies`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - expect(response.statusCode).toBe(200); - - const resources = response.json()["resources"]; - expect(resources.length).toBeGreaterThan(0); - - for (const resource of resources) { - expect(resource).toMatchObject({ - id: expect.any(String), - name: expect.any(String), - logo: expect.any(String), - role: expect.stringMatching(/owner|admin|member|guest/), - status: expect.stringMatching(/active|deactivated|invited/), - }); - - if (resource.plan) { - expect(resource.plan).toMatchObject({ - name: expect.any(String), - limits: expect.objectContaining({ - [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: expect.any(Number || undefined), - [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: expect.any(Number || undefined), - }), - }); - } - if (resources.stats) { - expect(resource.plan).toMatchObject({ - created_at: expect.any(Number), - total_members: expect.any(Number), - total_guests: expect.any(Number), - }); - } - } - done(); - }); - }); - - describe("The GET /companies/:company_id route", () => { - it("should 404 when company does not exists", async done => { - const response = await platform.app.inject({ - method: "GET", - url: `${url}/companies/11111111-1111-1111-1111-111111111111`, - }); - expect(response.statusCode).toBe(404); - done(); - }); - - it("should 200 when company exists", async done => { - const companyId = testDbService.company.id; - - const response = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${companyId}`, - }); - expect(response.statusCode).toBe(200); - - const json = response.json(); - - expect(json.resource).toMatchObject({ - id: expect.any(String), - name: expect.any(String), - logo: expect.any(String), - }); - - if (json.resource.plan) { - expect(json.resource.plan).toMatchObject({ - name: expect.any(String), - limits: expect.objectContaining({ - [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: expect.any(Number || undefined), - [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: expect.any(Number || undefined), - }), - }); - } - - expect(json.resource.stats).toMatchObject({ - created_at: expect.any(Number), - total_members: expect.any(Number), - total_guests: expect.any(Number), - total_messages: expect.any(Number), - }); - - done(); - }); - }); - - describe("User's device management", () => { - const deviceToken = "testDeviceToken"; - - describe("Register device (POST)", () => { - it("should 400 when type is not FCM", async done => { - const myId = testDbService.users[0].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "POST", - url: `${url}/devices`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - payload: { - resource: { - type: "another", - value: "value", - version: "version", - }, - }, - }); - - const resp = response.json(); - expect(response.statusCode).toBe(400); - expect(resp).toMatchObject({ - statusCode: 400, - error: "Bad Request", - message: "Type should be FCM only", - }); - done(); - }); - - it("should 200 when ok", async done => { - const firstId = testDbService.users[0].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: firstId }); - const response = await platform.app.inject({ - method: "POST", - url: `${url}/devices`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - payload: { - resource: { - type: "FCM", - value: deviceToken, - version: "1", - }, - }, - }); - - const resp = response.json(); - expect(response.statusCode).toBe(200); - - expect(resp.resource).toMatchObject({ - type: "FCM", - value: deviceToken, - version: "1", - }); - - const user = await testDbService.getUserFromDb({ id: firstId }); - expect(user.devices).toMatchObject([deviceToken]); - const device = await testDbService.getDeviceFromDb(deviceToken); - expect(device).toMatchObject({ - id: deviceToken, - user_id: firstId, - type: "FCM", - version: "1", - }); - - done(); - }); - - it("should 200 when register token to another person", async done => { - const firstId = testDbService.users[0].id; - const secondId = testDbService.users[1].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: secondId }); - const response = await platform.app.inject({ - method: "POST", - url: `${url}/devices`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - payload: { - resource: { - type: "FCM", - value: deviceToken, - version: "1", - }, - }, - }); - - const resp = response.json(); - expect(response.statusCode).toBe(200); - - expect(resp.resource).toMatchObject({ - type: "FCM", - value: deviceToken, - version: "1", - }); - - // second user should have now this token - let user = await testDbService.getUserFromDb({ id: secondId }); - expect(user.devices).toMatchObject([deviceToken]); - const device = await testDbService.getDeviceFromDb(deviceToken); - expect(device).toMatchObject({ - id: deviceToken, - user_id: secondId, - type: "FCM", - version: "1", - }); - - // and first — not - - user = await testDbService.getUserFromDb({ id: firstId }); - expect(user.devices).toMatchObject([]); - - done(); - }); - }); - describe("List registered devices (GET)", () => { - it("should 200 when request devices", async done => { - const myId = testDbService.users[1].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "GET", - url: `${url}/devices`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - const resp = response.json(); - expect(response.statusCode).toBe(200); - expect(resp).toMatchObject({ - resources: [ - { - type: "FCM", - value: "testDeviceToken", - version: "1", - }, - ], - }); - done(); - }); - }); - - describe("De-register device (DELETE)", () => { - it("should 200 when device not found for the user", async done => { - const myId = testDbService.users[1].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "DELETE", - url: `${url}/devices/somethingRandom`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(response.statusCode).toBe(204); - - const user = await testDbService.getUserFromDb({ id: myId }); - expect(user.devices).toMatchObject([deviceToken]); - const device = await testDbService.getDeviceFromDb(deviceToken); - expect(device).toMatchObject({ - id: deviceToken, - user_id: myId, - type: "FCM", - version: "1", - }); - - done(); - }); - - it("should 200 when device found and device should be removed", async done => { - const myId = testDbService.users[1].id; - - const jwtToken = await platform.auth.getJWTToken({ sub: myId }); - const response = await platform.app.inject({ - method: "DELETE", - url: `${url}/devices/${deviceToken}`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - expect(response.statusCode).toBe(204); - - const user = await testDbService.getUserFromDb({ id: myId }); - expect(user.devices).toMatchObject([]); - const device = await testDbService.getDeviceFromDb(deviceToken); - expect(device).toBeFalsy(); - - done(); - }); - }); - }); - - describe("Recent contacts", () => { - it("should return list of recent contacts of user", async done => { - // api = new Api(platform); - const channelUtils = getChannelUtils(platform); - - await testDbService.createDefault(platform); - - const channels = []; - - for (let i = 0; i < 5; i++) { - // const channel = channelUtils.getChannel(); - const directChannelIn = channelUtils.getDirectChannel(); - - const nextUser = await testDbService.createUser( - [{ id: platform.workspace.workspace_id, company_id: platform.workspace.company_id }], - { firstName: "FirstName" + i, lastName: "LastName" + i }, - ); - - const members = [platform.currentUser.id, nextUser.id]; - const directWorkspace: Workspace = { - company_id: platform.workspace.company_id, - workspace_id: ChannelVisibility.DIRECT, - }; - await Promise.all([ - // gr.services.channels.channels.save(channel, {}, getContext()), - gr.services.channels.channels.save( - directChannelIn, - { - members, - }, - { - ...{ - workspace: platform.workspace, - user: platform.currentUser, - }, - ...{ workspace: directWorkspace }, - }, - ), - ]); - channels.push(directChannelIn); - } - - await new Promise(resolve => setTimeout(resolve, 1000)); - - await e2e_createThread( - platform, - [ - { - company_id: platform.workspace.company_id, - created_at: 0, - created_by: "", - id: channels[2].id, - type: "channel", - workspace_id: "direct", - }, - ], - createMessage({ text: "Some message" }), - ); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - const jwtToken = await platform.auth.getJWTToken(); - - const response = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/users/recent`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - expect(response.statusCode).toBe(200); - - const result: ResourceListResponse = deserialize( - ResourceListResponse, - response.body, - ); - - expect(result.resources.length).toEqual(5); - expect(result.resources[0].first_name).toEqual("FirstName2"); - - done(); - }); - }); -}); diff --git a/twake/backend/node/test/e2e/websocket/auth.spec.ts b/twake/backend/node/test/e2e/websocket/auth.spec.ts deleted file mode 100644 index 2326101c..00000000 --- a/twake/backend/node/test/e2e/websocket/auth.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { afterEach, beforeEach, describe, it } from "@jest/globals"; -import { init, TestPlatform } from "../setup"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import io from "socket.io-client"; - -describe("The Websocket authentication", () => { - let platform: TestPlatform; - let socket: SocketIOClient.Socket; - - beforeEach(async ends => { - platform = await init({ - services: [ - "webserver", - "database", - "search", - "storage", - "message-queue", - "user", - "websocket", - "webserver", - "applications", - "auth", - "realtime", - "channels" /* FIXME: platform is not started if a business service is not in dependencies */, - "counter", - "statistics", - "platform-services", - ], - }); - - socket = io.connect("http://localhost:3000", { path: "/socket" }); - - ends(); - }); - - afterEach(async ends => { - platform && (await platform.tearDown()); - platform = null; - socket && socket.close(); - socket = null; - ends(); - }); - - describe("JWT-based Authentication", () => { - it("should not be able to connect without a JWT token", done => { - socket.connect(); - socket.on("connect", () => { - socket - .emit("authenticate", {}) - .on("authenticated", () => { - done(new Error("Should not occur")); - }) - .on("unauthorized", (msg: any) => { - done(); - }); - }); - }); - - it("should not be able to connect with something which is not a JWT token", done => { - socket.connect(); - socket.on("connect", () => { - socket - .emit("authenticate", { token: "Not a JWT token" }) - .on("authenticated", () => { - done(new Error("Should not occur")); - }) - .on("unauthorized", (msg: any) => { - done(); - }); - }); - }); - - it("should be able to connect with a JWT token", async done => { - const token = await platform.auth.getJWTToken(); - - socket.connect(); - socket.on("connect", () => { - socket - .emit("authenticate", { token }) - .on("authenticated", () => { - done(); - }) - .on("unauthorized", () => { - done(new Error("Should not occur")); - }); - }); - }); - }); -}); diff --git a/twake/backend/node/test/e2e/websocket/realtime.spec.ts b/twake/backend/node/test/e2e/websocket/realtime.spec.ts deleted file mode 100644 index d5199328..00000000 --- a/twake/backend/node/test/e2e/websocket/realtime.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; -import { init, TestPlatform } from "../setup"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import io from "socket.io-client"; - -describe("The Realtime API", () => { - let platform: TestPlatform; - let socket: SocketIOClient.Socket; - - beforeEach(async ends => { - platform = await init({ - services: [ - "webserver", - "database", - "search", - "storage", - "message-queue", - "applications", - "user", - "auth", - "websocket", - "realtime", - "channels" /* FIXME: platform is not started if a business service is not in dependencies */, - "counter", - "statistics", - "platform-services", - ], - }); - - socket = io.connect("http://localhost:3000", { path: "/socket" }); - - ends(); - }); - - afterEach(async ends => { - await platform.tearDown(); - platform = null; - socket && socket.close(); - socket = null; - - ends(); - }); - - describe("Joining rooms", () => { - it("should fail when token is not defined", async done => { - const token = await platform.auth.getJWTToken(); - const name = "/ping"; - - socket.on("connect", () => { - socket - .emit("authenticate", { token }) - .on("authenticated", () => { - socket.emit("realtime:join", { name }); - socket.on("realtime:join:error", (event: any) => { - expect(event.name).toEqual(name); - done(); - }); - socket.on("realtime:join:success", () => done(new Error("Should not occur"))); - }) - .on("unauthorized", () => { - done(new Error("Should not occur")); - }); - }); - }); - - it("should fail when token is not valid", async done => { - const token = await platform.auth.getJWTToken(); - const name = "/ping"; - const roomToken = "invalid token"; - - socket.on("connect", () => { - socket - .emit("authenticate", { token }) - .on("authenticated", () => { - socket.emit("realtime:join", { name, token: roomToken }); - socket.on("realtime:join:error", (event: any) => { - expect(event.name).toEqual(name); - done(); - }); - socket.on("realtime:join:success", () => done(new Error("Should not occur"))); - }) - .on("unauthorized", () => { - done(new Error("Should not occur")); - }); - }); - }); - - it("should receive a realtime:join:success when token is valid and room has been joined", async done => { - const token = await platform.auth.getJWTToken(); - const name = "test"; - const roomToken = "twake"; - - socket.on("connect", () => { - socket - .emit("authenticate", { token }) - .on("authenticated", () => { - socket.emit("realtime:join", { name, token: roomToken }); - socket.on("realtime:join:error", () => done(new Error("Should not occur"))); - socket.on("realtime:join:success", (event: any) => { - expect(event.name).toEqual(name); - done(); - }); - }) - .on("unauthorized", () => { - done(new Error("Should not occur")); - }); - }); - }); - }); - - describe("Leaving rooms", () => { - it("should not fail when room has not been joined first", async done => { - const token = await platform.auth.getJWTToken(); - const name = "roomtest"; - - socket.on("connect", () => { - socket - .emit("authenticate", { token }) - .on("authenticated", () => { - socket.emit("realtime:leave", { name }); - socket.on("realtime:leave:error", () => done(new Error("should not fail"))); - socket.on("realtime:leave:success", (event: any) => { - expect(event.name).toEqual(name); - done(); - }); - }) - .on("unauthorized", () => { - done(new Error("Should not occur")); - }); - }); - }); - - it("should send success when room has been joined first", async done => { - const token = await platform.auth.getJWTToken(); - const roomToken = "twake"; - const name = "roomtest"; - - socket.on("connect", () => { - socket - .emit("authenticate", { token }) - .on("authenticated", () => { - socket.emit("realtime:join", { name, token: roomToken }); - socket.emit("realtime:leave", { name }); - socket.on("realtime:leave:error", () => done(new Error("should not fail"))); - socket.on("realtime:leave:success", (event: any) => { - expect(event.name).toEqual(name); - done(); - }); - }) - .on("unauthorized", () => { - done(new Error("Should not occur")); - }); - }); - }); - }); -}); From de0a2d2066df43836a2e799770154f733f3fd379 Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Fri, 7 Apr 2023 10:09:25 +0100 Subject: [PATCH 3/8] fix: lint issues for unimplemented methods --- .../node/src/services/console/clients/remote.ts | 13 +++++++++++-- .../node/src/services/console/web/controller.ts | 2 +- .../backend/node/src/services/console/web/routes.ts | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/twake/backend/node/src/services/console/clients/remote.ts b/twake/backend/node/src/services/console/clients/remote.ts index 78db34b1..c5c2a29e 100644 --- a/twake/backend/node/src/services/console/clients/remote.ts +++ b/twake/backend/node/src/services/console/clients/remote.ts @@ -37,16 +37,17 @@ export class ConsoleRemoteClient implements ConsoleServiceClient { issuer: this.infos.issuer?.replace(/\/+$/, ""), jwksUri: this.infos.jwks_uri, // For local deployment create a https agent that ignore self signed certificate + // eslint-disable-next-line @typescript-eslint/no-var-requires requestAgent: new (require("https").Agent)({ rejectUnauthorized: this.infos.issuer.includes("example.com") ? false : true, }), }); } fetchCompanyInfo(consoleCompanyCode: string): Promise { - throw new Error("Method not implemented."); + throw new Error(`Method not implemented, ${consoleCompanyCode}.`); } resendVerificationEmail(email: string): Promise { - throw new Error("Method not implemented."); + throw new Error(`Method not implemented, ${email}.`); } private auth() { @@ -57,6 +58,7 @@ export class ConsoleRemoteClient implements ConsoleServiceClient { company: ConsoleCompany, user: CreateConsoleUser, ): Promise { + logger.info(`Method not implemented, ${company.id}, ${user.id}.`); return null; } @@ -65,22 +67,26 @@ export class ConsoleRemoteClient implements ConsoleServiceClient { user: UpdateConsoleUserRole, ): Promise { logger.info("Remote: updateUserRole"); + logger.info(`Method not implemented, ${company.id}, ${user.id}.`); return null; } async createCompany(company: CreateConsoleCompany): Promise { logger.info("Remote: createCompany"); + logger.info(`Method not implemented, ${company}.`); return null; } // eslint-disable-next-line @typescript-eslint/no-unused-vars addUserToTwake(user: CreateConsoleUser): Promise { logger.info("Remote: addUserToTwake"); + logger.info(`Method not implemented, ${user}.`); //should do noting for real console return Promise.resolve(undefined); } async updateLocalCompanyFromConsole(partialCompanyDTO: ConsoleHookCompany): Promise { + logger.info(`Method not implemented, ${partialCompanyDTO}.`); return null; } @@ -95,6 +101,9 @@ export class ConsoleRemoteClient implements ConsoleServiceClient { role => role.applications === undefined || role.applications.find(a => a.code === "twake"), ); + //REMOVE LATER + logger.info(`Roles are: ${roles}.`); + let user = await gr.services.users.getByConsoleId(userDTO.email); if (!user) { diff --git a/twake/backend/node/src/services/console/web/controller.ts b/twake/backend/node/src/services/console/web/controller.ts index 2fdbc35b..c38ea1a8 100644 --- a/twake/backend/node/src/services/console/web/controller.ts +++ b/twake/backend/node/src/services/console/web/controller.ts @@ -238,7 +238,7 @@ export class ConsoleController { private async userUpdated(code: string) { //Not implemented yet - throw CrudException.notImplemented("Unimplemented"); + throw CrudException.notImplemented(`Method not implemented, ${code}.`); } private async companyRemoved(content: ConsoleHookCompanyDeletedContent) { diff --git a/twake/backend/node/src/services/console/web/routes.ts b/twake/backend/node/src/services/console/web/routes.ts index 6fc96220..7edcd1b2 100644 --- a/twake/backend/node/src/services/console/web/routes.ts +++ b/twake/backend/node/src/services/console/web/routes.ts @@ -12,7 +12,7 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) const accessControl = async ( request: FastifyRequest<{ Body: ConsoleHookBody; Querystring: ConsoleHookQueryString }>, ) => { - throw fastify.httpErrors.notImplemented("Hook service doesn't exist anymore"); + throw fastify.httpErrors.notImplemented(`Hook service doesn't exist anymore, ${request}.`); }; fastify.route({ From af980a139bc96dbea4cf4b56fbe69b31358d411a Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Fri, 7 Apr 2023 11:01:19 +0100 Subject: [PATCH 4/8] fix: unit tests --- twake/backend/node/src/core/crypto/legacy.ts | 6 ++-- .../services/database/services/utils.test.ts | 31 +++++++++++++++++++ .../services/encryption/encryption.test.ts | 31 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 twake/backend/node/test/unit/core/services/database/services/utils.test.ts create mode 100644 twake/backend/node/test/unit/core/services/encryption/encryption.test.ts diff --git a/twake/backend/node/src/core/crypto/legacy.ts b/twake/backend/node/src/core/crypto/legacy.ts index f18479e0..63a61527 100644 --- a/twake/backend/node/src/core/crypto/legacy.ts +++ b/twake/backend/node/src/core/crypto/legacy.ts @@ -2,7 +2,7 @@ import { createDecipheriv } from "crypto"; import { CryptoResult } from "."; const PREFIX = "encrypted_"; -const DEFAULT_IV = "twake_constantiv"; +// const DEFAULT_IV = Buffer.from("twake_constantiv", "utf-8"); export default { decrypt, @@ -25,9 +25,9 @@ function decrypt(data: string, encryptionKey: string): CryptoResult { const salt = encryptedArray[1] ? Buffer.from(encryptedArray[1], "utf-8") : Buffer.from("", "utf-8"); - const iv = encryptedArray[2] + const iv = encryptedArray.length >= 3 ? Buffer.from(encryptedArray[2], "base64") - : Buffer.from(DEFAULT_IV, "utf-8"); + : Buffer.from("twake_constantiv", "utf-8"); const password = Buffer.concat([Buffer.from(encryptionKey, "hex"), salt], 32); const decipher = createDecipheriv("AES-256-CBC", password, iv); diff --git a/twake/backend/node/test/unit/core/services/database/services/utils.test.ts b/twake/backend/node/test/unit/core/services/database/services/utils.test.ts new file mode 100644 index 00000000..11fbac06 --- /dev/null +++ b/twake/backend/node/test/unit/core/services/database/services/utils.test.ts @@ -0,0 +1,31 @@ +import "reflect-metadata"; +import { describe, expect, it } from "@jest/globals"; +import { + fromMongoDbOrderable, + toMongoDbOrderable, +} from "../../../../../../src/core/platform/services/database/services/orm/utils"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import _ from "lodash"; +import { v1 as uuidv1 } from "uuid"; + +describe("The MongoDb to Orderable module", () => { + describe("The to/from orderable function", () => { + it("should be unique", () => { + const uuid1 = toMongoDbOrderable(uuidv1()); + const uuid2 = toMongoDbOrderable(uuidv1()); + const uuid3 = toMongoDbOrderable(uuidv1()); + const uuid4 = toMongoDbOrderable(uuidv1()); + + expect(_.uniq([uuid1, uuid2, uuid3, uuid4]).length).toBe(4); + }); + + it("should convert both ways", () => { + const uuid = uuidv1(); + const orderable = toMongoDbOrderable(uuid); + + expect(fromMongoDbOrderable(orderable)).toBe(uuid); + expect(orderable).toBe(toMongoDbOrderable(fromMongoDbOrderable(orderable))); + }); + }); +}); diff --git a/twake/backend/node/test/unit/core/services/encryption/encryption.test.ts b/twake/backend/node/test/unit/core/services/encryption/encryption.test.ts new file mode 100644 index 00000000..83f7ea1c --- /dev/null +++ b/twake/backend/node/test/unit/core/services/encryption/encryption.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "@jest/globals"; +import { decrypt } from "../../../../../src/core/crypto/index"; +import v2 from "../../../../../src/core/crypto/v2"; +import v1 from "../../../../../src/core/crypto/v1"; +import legacy from "../../../../../src/core/crypto/legacy"; + +describe("Encryption", () => { + const encryptionKey = "a7c06651a7c063bb3e90c0c9a17eab88ab8977665127196a"; + + describe("The encrypt/decrypt functions", () => { + it("should successfully describe legacy encrypted values", async () => { + const legacyEncrypted = "encrypted_DwMLnKhuFbIanqBJPA5rcw=="; + + expect(legacy.decrypt(legacyEncrypted, encryptionKey).data).toBe("My company"); + expect(decrypt(legacyEncrypted, encryptionKey).data).toBe("My company"); + }); + + it("should successfully describe all versions", async () => { + const myData = { key: "some data" }; + + const v1Encrypted = v1.encrypt(myData, encryptionKey); + const v2Encrypted = v2.encrypt(myData, encryptionKey); + + expect(v1.decrypt(v1Encrypted.data, encryptionKey).data).toMatchObject(myData); + expect(v2.decrypt(v2Encrypted.data, encryptionKey).data).toMatchObject(myData); + + expect(decrypt(v1Encrypted.data, encryptionKey).data).toMatchObject(myData); + expect(decrypt(v2Encrypted.data, encryptionKey).data).toMatchObject(myData); + }); + }); +}); From c97ec52a112ee24a6dac85898428816c96b20d51 Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Fri, 7 Apr 2023 11:16:04 +0100 Subject: [PATCH 5/8] ref: e2e tests --- twake/backend/node/src/core/crypto/legacy.ts | 7 +- .../node/test/e2e/users/users.search.spec.ts | 149 ++++ .../backend/node/test/e2e/users/users.spec.ts | 645 ++++++++++++++++++ 3 files changed, 798 insertions(+), 3 deletions(-) create mode 100644 twake/backend/node/test/e2e/users/users.search.spec.ts create mode 100644 twake/backend/node/test/e2e/users/users.spec.ts diff --git a/twake/backend/node/src/core/crypto/legacy.ts b/twake/backend/node/src/core/crypto/legacy.ts index 63a61527..a8f3bf8b 100644 --- a/twake/backend/node/src/core/crypto/legacy.ts +++ b/twake/backend/node/src/core/crypto/legacy.ts @@ -25,9 +25,10 @@ function decrypt(data: string, encryptionKey: string): CryptoResult { const salt = encryptedArray[1] ? Buffer.from(encryptedArray[1], "utf-8") : Buffer.from("", "utf-8"); - const iv = encryptedArray.length >= 3 - ? Buffer.from(encryptedArray[2], "base64") - : Buffer.from("twake_constantiv", "utf-8"); + const iv = + encryptedArray.length >= 3 + ? Buffer.from(encryptedArray[2], "base64") + : Buffer.from("twake_constantiv", "utf-8"); const password = Buffer.concat([Buffer.from(encryptionKey, "hex"), salt], 32); const decipher = createDecipheriv("AES-256-CBC", password, iv); diff --git a/twake/backend/node/test/e2e/users/users.search.spec.ts b/twake/backend/node/test/e2e/users/users.search.spec.ts new file mode 100644 index 00000000..b95d1320 --- /dev/null +++ b/twake/backend/node/test/e2e/users/users.search.spec.ts @@ -0,0 +1,149 @@ +import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import { TestDbService } from "../utils.prepare.db"; +import { v1 as uuidv1 } from "uuid"; + +describe("The /users API", () => { + const url = "/internal/services/users/v1"; + let platform: TestPlatform; + + beforeEach(async ends => { + platform = await init({ + services: [ + "database", + "search", + "message-queue", + "websocket", + "webserver", + "user", + "auth", + "applications", + "storage", + "counter", + "workspaces", + "console", + "statistics", + "platform-services", + ], + }); + ends(); + }); + + afterEach(async ends => { + platform && (await platform.tearDown()); + platform = null; + ends(); + }); + + describe("The GET /users/?search=... route", () => { + it("Should find the searched users", async done => { + const testDbService = new TestDbService(platform); + await testDbService.createCompany(platform.workspace.company_id); + const workspacePk = { + id: platform.workspace.workspace_id, + company_id: platform.workspace.company_id, + }; + const workspacePk2 = { + id: uuidv1(), + company_id: uuidv1(), + }; + await testDbService.createWorkspace(workspacePk); + await testDbService.createWorkspace(workspacePk2); + await testDbService.createUser([workspacePk], { + firstName: "Ha", + lastName: "Nguyen", + email: "hnguyen@twake.app", + }); + await testDbService.createUser([workspacePk], { + firstName: "Harold", + lastName: "Georges", + email: "hgeorges@twake.app", + }); + await testDbService.createUser([workspacePk], { + firstName: "Bob", + lastName: "Smith", + email: "bob@twake.app", + }); + await testDbService.createUser([workspacePk], { + firstName: "Bob", + lastName: "Rabiot", + email: "rabiot.b@twake.app", + }); + await testDbService.createUser([workspacePk, workspacePk2], { + firstName: "Bob", + lastName: "Smith-Rabiot", + email: "rbs@twake.app", + }); + await testDbService.createUser([workspacePk], { + firstName: "Alexïs", + lastName: "Goélâns", + email: "alexis.goelans@twake.app", + }); + + //Wait for indexation to happen + await new Promise(r => setTimeout(r, 5000)); + + let resources = await search("ha"); + expect(resources.length).toBe(2); + + resources = await search("bob rabiot"); + + expect(resources.map(e => e.email).includes("rabiot.b@twake.app")).toBe(true); + expect(resources.map(e => e.email).includes("rbs@twake.app")).toBe(true); + expect(resources.map(e => e.email).includes("bob@twake.app")).toBe(true); + + resources = await search("alexis"); + expect(resources[0].email).toBe("alexis.goelans@twake.app"); + + resources = await search("ALEXIS"); + expect(resources[0].email).toBe("alexis.goelans@twake.app"); + + resources = await search("AleXis"); + expect(resources[0].email).toBe("alexis.goelans@twake.app"); + + resources = await search("alex"); + expect(resources[0].email).toBe("alexis.goelans@twake.app"); + + resources = await search("àlèXïs"); + expect(resources[0].email).toBe("alexis.goelans@twake.app"); + + resources = await search("rbs"); + expect(resources[0].email).toBe("rbs@twake.app"); + + resources = await search("rbs@twake.app"); + expect(resources[0].email).toBe("rbs@twake.app"); + + resources = await search("bob", workspacePk2.company_id); + expect(resources.length).toBe(1); + + resources = await search("rbs@twake.app", workspacePk.company_id); + expect(resources[0].email).toBe("rbs@twake.app"); + + resources = await search("rbs@twake.app", uuidv1()); + expect(resources.length).toBe(0); + + done(); + }); + }); + + async function search(search: string, companyId?: string): Promise { + const jwtToken = await platform.auth.getJWTToken(); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + query: { + search: search, + ...(companyId ? { search_company_id: companyId } : {}), + }, + }); + + expect(response.statusCode).toBe(200); + const json = response.json(); + expect(json).toMatchObject({ resources: expect.any(Array) }); + const resources = json.resources; + return resources; + } +}); diff --git a/twake/backend/node/test/e2e/users/users.spec.ts b/twake/backend/node/test/e2e/users/users.spec.ts new file mode 100644 index 00000000..a8c03130 --- /dev/null +++ b/twake/backend/node/test/e2e/users/users.spec.ts @@ -0,0 +1,645 @@ +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import { TestDbService } from "../utils.prepare.db"; +import { v1 as uuidv1 } from "uuid"; +import { CompanyLimitsEnum, UserObject } from "../../../src/services/user/web/types"; +import { ChannelVisibility } from "../../../src/services/channels/types"; +import gr from "../../../src/services/global-resolver"; +import { ResourceListResponse, Workspace } from "../../../src/utils/types"; +import { ChannelSaveOptions } from "../../../src/services/channels/web/types"; +import { createMessage, e2e_createThread } from "../messages/utils"; +import { deserialize } from "class-transformer"; +import { get as getChannelUtils } from "../channels/utils"; + +describe("The /users API", () => { + const url = "/internal/services/users/v1"; + let platform: TestPlatform; + + let testDbService: TestDbService; + + const nonExistentId = uuidv1(); + + beforeEach(async ends => { + platform = await init(); + ends(); + }); + afterEach(async ends => { + await platform.tearDown(); + platform = null; + ends(); + }); + + beforeAll(async ends => { + const platform = await init({ + services: [ + "database", + "search", + "message-queue", + "websocket", + "applications", + "webserver", + "user", + "auth", + "storage", + "counter", + "console", + "workspaces", + "statistics", + "platform-services", + ], + }); + + testDbService = await TestDbService.getInstance(platform); + await testDbService.createCompany(); + const workspacePk = { id: uuidv1(), company_id: testDbService.company.id }; + await testDbService.createWorkspace(workspacePk); + await testDbService.createUser([workspacePk], { + workspaceRole: "moderator", + companyRole: "admin", + email: "admin@admin.admin", + username: "adminuser", + firstName: "admin", + }); + await testDbService.createUser([workspacePk]); + + ends(); + }); + + afterAll(async ends => { + ends(); + }); + + describe("The GET /users/:id route", () => { + it("should 401 when not authenticated", async done => { + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users/1`, + }); + + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 404 when user does not exists", async done => { + const jwtToken = await platform.auth.getJWTToken({ sub: testDbService.users[0].id }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users/${nonExistentId}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + + expect(response.statusCode).toBe(404); + expect(response.json()).toEqual({ + error: "Not Found", + message: `User ${nonExistentId} not found`, + statusCode: 404, + }); + done(); + }); + + it("should 200 and big response for myself", async done => { + const myId = testDbService.users[0].id; + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users/${myId}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + + expect(resource).toMatchObject({ + id: myId, + provider: expect.any(String), + provider_id: expect.any(String), + email: expect.any(String), + is_verified: expect.any(Boolean), + picture: expect.any(String), + first_name: expect.any(String), + last_name: expect.any(String), + created_at: expect.any(Number), + deleted: expect.any(Boolean), + status: expect.any(String), + last_activity: expect.any(Number), + + //Below is only if this is myself + + preference: expect.objectContaining({ + locale: expect.any(String), + timezone: expect.any(Number), + }), + }); + + expect(resource["companies"]).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + role: expect.stringMatching(/owner|admin|member|guest/), + status: expect.stringMatching(/active|deactivated|invited/), + company: { + id: expect.any(String), + name: expect.any(String), + logo: expect.any(String), + }, + }), + ]), + ); + + done(); + }); + + it("should 200 and short response for another user", async done => { + const myId = testDbService.users[0].id; + const anotherUserId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users/${anotherUserId}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + + expect(resource).toMatchObject({ + id: anotherUserId, + provider: expect.any(String), + provider_id: expect.any(String), + email: expect.any(String), + is_verified: expect.any(Boolean), + picture: expect.any(String), + first_name: expect.any(String), + last_name: expect.any(String), + created_at: expect.any(Number), + deleted: expect.any(Boolean), + status: expect.any(String), + last_activity: expect.any(Number), + }); + + expect(resource).not.toMatchObject({ + locale: expect.anything(), + timezone: expect.anything(), + companies: expect.anything(), + }); + + done(); + }); + }); + + describe("The GET /users route", () => { + it("should 401 when user is not authenticated", async done => { + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users`, + }); + + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 200 with array of users", async done => { + const myId = testDbService.users[0].id; + const anotherUserId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + query: { + user_ids: `${myId},${anotherUserId}`, + company_ids: "fd96c8a8-ae77-11eb-a1a1-0242ac120005", + }, + }); + + expect(response.statusCode).toBe(200); + const json = response.json(); + expect(json).toMatchObject({ resources: expect.any(Array) }); + const resources = json.resources; + + done(); + }); + }); + + describe("The GET /users/:user_id/companies route", () => { + it("should 401 when not authenticated", async done => { + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users/1/companies`, + }); + + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 404 when user does not exists", async done => { + const jwtToken = await platform.auth.getJWTToken({ sub: testDbService.users[0].id }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users/${nonExistentId}/companies`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + + expect(response.statusCode).toBe(404); + expect(response.json()).toEqual({ + error: "Not Found", + message: `User ${nonExistentId} not found`, + statusCode: 404, + }); + done(); + }); + + it("should 200 and on correct request", async done => { + const myId = testDbService.users[0].id; + const anotherUserId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/users/${anotherUserId}/companies`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + + expect(response.statusCode).toBe(200); + + const resources = response.json()["resources"]; + expect(resources.length).toBeGreaterThan(0); + + for (const resource of resources) { + expect(resource).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + logo: expect.any(String), + role: expect.stringMatching(/owner|admin|member|guest/), + status: expect.stringMatching(/active|deactivated|invited/), + }); + + if (resource.plan) { + expect(resource.plan).toMatchObject({ + name: expect.any(String), + limits: expect.objectContaining({ + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: expect.any(Number || undefined), + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: expect.any(Number || undefined), + }), + }); + } + if (resources.stats) { + expect(resource.plan).toMatchObject({ + created_at: expect.any(Number), + total_members: expect.any(Number), + total_guests: expect.any(Number), + }); + } + } + done(); + }); + }); + + describe("The GET /companies/:company_id route", () => { + it("should 404 when company does not exists", async done => { + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/11111111-1111-1111-1111-111111111111`, + }); + expect(response.statusCode).toBe(404); + done(); + }); + + it("should 200 when company exists", async done => { + const companyId = testDbService.company.id; + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}`, + }); + expect(response.statusCode).toBe(200); + + const json = response.json(); + + expect(json.resource).toMatchObject({ + id: expect.any(String), + name: expect.any(String), + logo: expect.any(String), + }); + + if (json.resource.plan) { + expect(json.resource.plan).toMatchObject({ + name: expect.any(String), + limits: expect.objectContaining({ + [CompanyLimitsEnum.CHAT_MESSAGE_HISTORY_LIMIT]: expect.any(Number || undefined), + [CompanyLimitsEnum.COMPANY_MEMBERS_LIMIT]: expect.any(Number || undefined), + }), + }); + } + + expect(json.resource.stats).toMatchObject({ + created_at: expect.any(Number), + total_members: expect.any(Number), + total_guests: expect.any(Number), + total_messages: expect.any(Number), + }); + + done(); + }); + }); + + describe("User's device management", () => { + const deviceToken = "testDeviceToken"; + + describe("Register device (POST)", () => { + it("should 400 when type is not FCM", async done => { + const myId = testDbService.users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "POST", + url: `${url}/devices`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + payload: { + resource: { + type: "another", + value: "value", + version: "version", + }, + }, + }); + + const resp = response.json(); + expect(response.statusCode).toBe(400); + expect(resp).toMatchObject({ + statusCode: 400, + error: "Bad Request", + message: "Type should be FCM only", + }); + done(); + }); + + it("should 200 when ok", async done => { + const firstId = testDbService.users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: firstId }); + const response = await platform.app.inject({ + method: "POST", + url: `${url}/devices`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + payload: { + resource: { + type: "FCM", + value: deviceToken, + version: "1", + }, + }, + }); + + const resp = response.json(); + expect(response.statusCode).toBe(200); + + expect(resp.resource).toMatchObject({ + type: "FCM", + value: deviceToken, + version: "1", + }); + + const user = await testDbService.getUserFromDb({ id: firstId }); + expect(user.devices).toMatchObject([deviceToken]); + const device = await testDbService.getDeviceFromDb(deviceToken); + expect(device).toMatchObject({ + id: deviceToken, + user_id: firstId, + type: "FCM", + version: "1", + }); + + done(); + }); + + it("should 200 when register token to another person", async done => { + const firstId = testDbService.users[0].id; + const secondId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: secondId }); + const response = await platform.app.inject({ + method: "POST", + url: `${url}/devices`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + payload: { + resource: { + type: "FCM", + value: deviceToken, + version: "1", + }, + }, + }); + + const resp = response.json(); + expect(response.statusCode).toBe(200); + + expect(resp.resource).toMatchObject({ + type: "FCM", + value: deviceToken, + version: "1", + }); + + // second user should have now this token + let user = await testDbService.getUserFromDb({ id: secondId }); + expect(user.devices).toMatchObject([deviceToken]); + const device = await testDbService.getDeviceFromDb(deviceToken); + expect(device).toMatchObject({ + id: deviceToken, + user_id: secondId, + type: "FCM", + version: "1", + }); + + // and first — not + + user = await testDbService.getUserFromDb({ id: firstId }); + expect(user.devices).toMatchObject([]); + + done(); + }); + }); + describe("List registered devices (GET)", () => { + it("should 200 when request devices", async done => { + const myId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/devices`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + + const resp = response.json(); + expect(response.statusCode).toBe(200); + expect(resp).toMatchObject({ + resources: [ + { + type: "FCM", + value: "testDeviceToken", + version: "1", + }, + ], + }); + done(); + }); + }); + + describe("De-register device (DELETE)", () => { + it("should 200 when device not found for the user", async done => { + const myId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/devices/somethingRandom`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(response.statusCode).toBe(204); + + const user = await testDbService.getUserFromDb({ id: myId }); + expect(user.devices).toMatchObject([deviceToken]); + const device = await testDbService.getDeviceFromDb(deviceToken); + expect(device).toMatchObject({ + id: deviceToken, + user_id: myId, + type: "FCM", + version: "1", + }); + + done(); + }); + + it("should 200 when device found and device should be removed", async done => { + const myId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/devices/${deviceToken}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + expect(response.statusCode).toBe(204); + + const user = await testDbService.getUserFromDb({ id: myId }); + expect(user.devices).toMatchObject([]); + const device = await testDbService.getDeviceFromDb(deviceToken); + expect(device).toBeFalsy(); + + done(); + }); + }); + }); + + describe("Recent contacts", () => { + it("should return list of recent contacts of user", async done => { + // api = new Api(platform); + const channelUtils = getChannelUtils(platform); + + await testDbService.createDefault(platform); + + const channels = []; + + for (let i = 0; i < 5; i++) { + // const channel = channelUtils.getChannel(); + const directChannelIn = channelUtils.getDirectChannel(); + + const nextUser = await testDbService.createUser( + [{ id: platform.workspace.workspace_id, company_id: platform.workspace.company_id }], + { firstName: "FirstName" + i, lastName: "LastName" + i }, + ); + + const members = [platform.currentUser.id, nextUser.id]; + const directWorkspace: Workspace = { + company_id: platform.workspace.company_id, + workspace_id: ChannelVisibility.DIRECT, + }; + await Promise.all([ + // gr.services.channels.channels.save(channel, {}, getContext()), + gr.services.channels.channels.save( + directChannelIn, + { + members, + }, + { + ...{ + workspace: platform.workspace, + user: platform.currentUser, + }, + ...{ workspace: directWorkspace }, + }, + ), + ]); + channels.push(directChannelIn); + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + + await e2e_createThread( + platform, + [ + { + company_id: platform.workspace.company_id, + created_at: 0, + created_by: "", + id: channels[2].id, + type: "channel", + workspace_id: "direct", + }, + ], + createMessage({ text: "Some message" }), + ); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const jwtToken = await platform.auth.getJWTToken(); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/users/recent`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }); + + expect(response.statusCode).toBe(200); + + const result: ResourceListResponse = deserialize( + ResourceListResponse, + response.body, + ); + + expect(result.resources.length).toEqual(5); + expect(result.resources[0].first_name).toEqual("FirstName2"); + + done(); + }); + }); +}); From c4b9fba6219fd31fb3383c6b1577d21c0bc58348 Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Fri, 7 Apr 2023 11:32:28 +0100 Subject: [PATCH 6/8] fix: removed channel from user e2e tests --- .../backend/node/test/e2e/users/users.spec.ts | 94 +------------------ .../backend/node/test/e2e/utils.prepare.db.ts | 13 --- 2 files changed, 1 insertion(+), 106 deletions(-) diff --git a/twake/backend/node/test/e2e/users/users.spec.ts b/twake/backend/node/test/e2e/users/users.spec.ts index a8c03130..742d6cf8 100644 --- a/twake/backend/node/test/e2e/users/users.spec.ts +++ b/twake/backend/node/test/e2e/users/users.spec.ts @@ -2,14 +2,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from import { init, TestPlatform } from "../setup"; import { TestDbService } from "../utils.prepare.db"; import { v1 as uuidv1 } from "uuid"; -import { CompanyLimitsEnum, UserObject } from "../../../src/services/user/web/types"; -import { ChannelVisibility } from "../../../src/services/channels/types"; -import gr from "../../../src/services/global-resolver"; -import { ResourceListResponse, Workspace } from "../../../src/utils/types"; -import { ChannelSaveOptions } from "../../../src/services/channels/web/types"; -import { createMessage, e2e_createThread } from "../messages/utils"; -import { deserialize } from "class-transformer"; -import { get as getChannelUtils } from "../channels/utils"; +import { CompanyLimitsEnum } from "../../../src/services/user/web/types"; describe("The /users API", () => { const url = "/internal/services/users/v1"; @@ -557,89 +550,4 @@ describe("The /users API", () => { }); }); }); - - describe("Recent contacts", () => { - it("should return list of recent contacts of user", async done => { - // api = new Api(platform); - const channelUtils = getChannelUtils(platform); - - await testDbService.createDefault(platform); - - const channels = []; - - for (let i = 0; i < 5; i++) { - // const channel = channelUtils.getChannel(); - const directChannelIn = channelUtils.getDirectChannel(); - - const nextUser = await testDbService.createUser( - [{ id: platform.workspace.workspace_id, company_id: platform.workspace.company_id }], - { firstName: "FirstName" + i, lastName: "LastName" + i }, - ); - - const members = [platform.currentUser.id, nextUser.id]; - const directWorkspace: Workspace = { - company_id: platform.workspace.company_id, - workspace_id: ChannelVisibility.DIRECT, - }; - await Promise.all([ - // gr.services.channels.channels.save(channel, {}, getContext()), - gr.services.channels.channels.save( - directChannelIn, - { - members, - }, - { - ...{ - workspace: platform.workspace, - user: platform.currentUser, - }, - ...{ workspace: directWorkspace }, - }, - ), - ]); - channels.push(directChannelIn); - } - - await new Promise(resolve => setTimeout(resolve, 1000)); - - await e2e_createThread( - platform, - [ - { - company_id: platform.workspace.company_id, - created_at: 0, - created_by: "", - id: channels[2].id, - type: "channel", - workspace_id: "direct", - }, - ], - createMessage({ text: "Some message" }), - ); - - await new Promise(resolve => setTimeout(resolve, 1000)); - - const jwtToken = await platform.auth.getJWTToken(); - - const response = await platform.app.inject({ - method: "GET", - url: `${url}/companies/${platform.workspace.company_id}/users/recent`, - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }); - - expect(response.statusCode).toBe(200); - - const result: ResourceListResponse = deserialize( - ResourceListResponse, - response.body, - ); - - expect(result.resources.length).toEqual(5); - expect(result.resources[0].first_name).toEqual("FirstName2"); - - done(); - }); - }); }); diff --git a/twake/backend/node/test/e2e/utils.prepare.db.ts b/twake/backend/node/test/e2e/utils.prepare.db.ts index b8cdfa3c..148ba957 100644 --- a/twake/backend/node/test/e2e/utils.prepare.db.ts +++ b/twake/backend/node/test/e2e/utils.prepare.db.ts @@ -15,8 +15,6 @@ import Repository from "../../src/core/platform/services/database/services/orm/r import Device from "../../src/services/user/entities/device"; import gr from "../../src/services/global-resolver"; -import { Channel } from "../../src/services/channels/entities"; -import { get as getChannelUtils } from "./channels/utils"; export type uuid = string; @@ -259,15 +257,4 @@ export class TestDbService { defaultWorkspace() { return this.workspaces[0].workspace; } - - async createChannel(userId): Promise { - const channelUtils = getChannelUtils(this.testPlatform); - const channel = channelUtils.getChannel(userId); - const creationResult = await gr.services.channels.channels.save( - channel, - {}, - channelUtils.getContext({ id: userId }), - ); - return creationResult.entity; - } } From b26d6b33c16daa791f2c49377c71475bf7bb825b Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Mon, 10 Apr 2023 11:58:53 +0100 Subject: [PATCH 7/8] fix: updated e2e tests --- twake/backend/node/config/default.json | 4 - .../src/core/platform/framework/logger.ts | 8 +- .../entities/application.search.ts | 33 ++ .../applications/entities/application.ts | 212 ++++++++ .../entities/company-application.ts | 40 ++ .../node/src/services/applications/index.ts | 24 + .../src/services/applications/realtime.ts | 13 + .../applications/services/applications.ts | 143 +++++ .../services/company-applications.ts | 188 +++++++ .../services/applications/services/hooks.ts | 96 ++++ .../services/internal-event-to-hooks.ts | 64 +++ .../web/controllers/applications.ts | 296 ++++++++++ .../web/controllers/company-applications.ts | 119 ++++ .../applications/web/controllers/index.ts | 1 + .../src/services/applications/web/index.ts | 12 + .../src/services/applications/web/routes.ts | 161 ++++++ .../src/services/applications/web/schemas.ts | 129 +++++ .../src/services/applications/web/types.ts | 15 + .../node/test/e2e/documents/assets/test.txt | 1 + .../test/e2e/documents/documents-tab.spec.ts | 183 +++++++ .../node/test/e2e/documents/documents.spec.ts | 238 ++++++++ .../backend/node/test/e2e/documents/utils.ts | 116 ++++ .../e2e/workspaces/workspace-users.spec.ts | 469 ++++++++++++++++ .../test/e2e/workspaces/workspaces.spec.ts | 513 ++++++++++++++++++ 24 files changed, 3067 insertions(+), 11 deletions(-) create mode 100644 twake/backend/node/src/services/applications/entities/application.search.ts create mode 100644 twake/backend/node/src/services/applications/entities/application.ts create mode 100644 twake/backend/node/src/services/applications/entities/company-application.ts create mode 100644 twake/backend/node/src/services/applications/index.ts create mode 100644 twake/backend/node/src/services/applications/realtime.ts create mode 100644 twake/backend/node/src/services/applications/services/applications.ts create mode 100644 twake/backend/node/src/services/applications/services/company-applications.ts create mode 100644 twake/backend/node/src/services/applications/services/hooks.ts create mode 100644 twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts create mode 100644 twake/backend/node/src/services/applications/web/controllers/applications.ts create mode 100644 twake/backend/node/src/services/applications/web/controllers/company-applications.ts create mode 100644 twake/backend/node/src/services/applications/web/controllers/index.ts create mode 100644 twake/backend/node/src/services/applications/web/index.ts create mode 100644 twake/backend/node/src/services/applications/web/routes.ts create mode 100644 twake/backend/node/src/services/applications/web/schemas.ts create mode 100644 twake/backend/node/src/services/applications/web/types.ts create mode 100644 twake/backend/node/test/e2e/documents/assets/test.txt create mode 100644 twake/backend/node/test/e2e/documents/documents-tab.spec.ts create mode 100644 twake/backend/node/test/e2e/documents/documents.spec.ts create mode 100644 twake/backend/node/test/e2e/documents/utils.ts create mode 100644 twake/backend/node/test/e2e/workspaces/workspace-users.spec.ts create mode 100644 twake/backend/node/test/e2e/workspaces/workspaces.spec.ts diff --git a/twake/backend/node/config/default.json b/twake/backend/node/config/default.json index 986258b2..3aaa8701 100644 --- a/twake/backend/node/config/default.json +++ b/twake/backend/node/config/default.json @@ -168,15 +168,11 @@ "tracker", "general", "user", - "applications-api", - "applications", "channels", "notifications", - "messages", "files", "workspaces", "console", - "previews", "counter", "statistics", "cron", diff --git a/twake/backend/node/src/core/platform/framework/logger.ts b/twake/backend/node/src/core/platform/framework/logger.ts index 8c0aa335..b07a2d93 100644 --- a/twake/backend/node/src/core/platform/framework/logger.ts +++ b/twake/backend/node/src/core/platform/framework/logger.ts @@ -8,13 +8,7 @@ export type TwakeLogger = pino.Logger; export const logger = pino({ name: "TwakeApp", level: config.get("level", "info") || "info", - prettyPrint: - process.env.NODE_ENV?.indexOf("test") > -1 - ? { - translateTime: "HH:MM:ss Z", - ignore: "pid,hostname,name", - } - : false, + prettyPrint: false, }); export const getLogger = (name?: string): TwakeLogger => diff --git a/twake/backend/node/src/services/applications/entities/application.search.ts b/twake/backend/node/src/services/applications/entities/application.search.ts new file mode 100644 index 00000000..669e0245 --- /dev/null +++ b/twake/backend/node/src/services/applications/entities/application.search.ts @@ -0,0 +1,33 @@ +import Application, { TYPE } from "./application"; + +export default { + index: TYPE, + source: (entity: Application) => { + return { + company_id: entity.company_id, + name: entity.identity.name, + description: entity.identity.description, + categories: entity.identity.categories, + compatibility: entity.identity.compatibility, + published: entity.publication.published, + created_at: entity.stats.created_at, + }; + }, + mongoMapping: { + text: { + name: "text", + description: "text", + }, + }, + esMapping: { + properties: { + company_id: { type: "keyword" }, + name: { type: "text", index_prefixes: { min_chars: 1 } }, + description: { type: "text" }, + categories: { type: "keyword" }, + compatibility: { type: "keyword" }, + published: { type: "boolean" }, + created_at: { type: "number" }, + }, + }, +}; diff --git a/twake/backend/node/src/services/applications/entities/application.ts b/twake/backend/node/src/services/applications/entities/application.ts new file mode 100644 index 00000000..21927414 --- /dev/null +++ b/twake/backend/node/src/services/applications/entities/application.ts @@ -0,0 +1,212 @@ +import { Type } from "class-transformer"; +import _, { merge } from "lodash"; +import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; +import search from "./application.search"; + +export const TYPE = "applications"; + +@Entity(TYPE, { + primaryKey: ["id"], + type: TYPE, + search, +}) +export default class Application { + @Type(() => String) + @Column("id", "timeuuid", { generator: "timeuuid" }) + id: string; + + @Type(() => String) + @Column("group_id", "timeuuid") + company_id: string; + + @Column("is_default", "boolean") + is_default: boolean; + + @Column("identity", "json") + identity: ApplicationIdentity; + + //This information is private to the application, make sure not to disclose it + @Column("api", "encoded_json") + api: ApplicationApi; + + @Column("access", "json") + access: ApplicationAccess; + + @Column("display", "json") + display: ApplicationDisplay; + + @Column("publication", "json") + publication: ApplicationPublication; + + @Column("stats", "json") + stats: ApplicationStatistics; + + getPublicObject(): PublicApplicationObject { + const i = _.pick( + this, + "id", + "company_id", + "is_default", + "identity", + "access", + "display", + "publication", + "stats", + ); + + i.is_default = !!i.is_default; + return i; + } + + getApplicationObject(): ApplicationObject { + const i = _.pick( + this, + "id", + "company_id", + "is_default", + "identity", + "access", + "display", + "publication", + "stats", + "api", + ); + + i.is_default = !!i.is_default; + return i; + } +} + +export type PublicApplicationObject = Pick< + Application, + "id" | "company_id" | "is_default" | "identity" | "access" | "display" | "publication" | "stats" +>; + +export type ApplicationObject = Pick< + Application, + | "id" + | "company_id" + | "is_default" + | "identity" + | "access" + | "display" + | "publication" + | "stats" + | "api" +>; + +export type ApplicationPrimaryKey = { id: string }; + +export function getInstance(message: Application): Application { + return merge(new Application(), message); +} + +export type ApplicationIdentity = { + code: string; + name: string; + icon: string; + description: string; + website: string; + categories: string[]; + compatibility: "twake"[]; + repository?: string; +}; + +export type ApplicationPublication = { + published: boolean; //Publication accepted // RO + requested: boolean; //Publication requested +}; + +export type ApplicationStatistics = { + created_at: number; // RO + updated_at: number; // RO + version: number; // RO +}; + +export type ApplicationApi = { + hooks_url: string; + allowed_ips: string; + private_key: string; // RO +}; + +type ApplicationScopes = + | "files" + | "applications" + | "workspaces" + | "users" + | "messages" + | "channels"; + +export type ApplicationAccess = { + read: ApplicationScopes[]; + write: ApplicationScopes[]; + delete: ApplicationScopes[]; + hooks: ApplicationScopes[]; +}; + +export type ApplicationDisplay = { + twake: { + files?: { + editor?: { + preview_url: string; //Open a preview inline (iframe) + edition_url: string; //Url to edit the file (full screen) + extensions?: string[]; //Main extensions app can read + // if file was created by the app, then the app is able to edit with or without extension + empty_files?: { + url: string; // "https://[...]/empty.docx"; + filename: string; // "Untitled.docx"; + name: string; // "Word Document"; + }[]; + }; + actions?: //List of action that can apply on a file + { + name: string; + id: string; + }[]; + }; + + //Chat plugin + chat?: { + input?: + | true + | { + icon?: string; //If defined replace original icon url of your app + type?: "file" | "call"; //To add in existing apps folder / default icon + }; + commands?: { + command: string; // my_app mycommand + description: string; + }[]; + actions?: //List of action that can apply on a message + { + name: string; + id: string; + }[]; + }; + + //Allow app to appear as a bot user in direct chat + direct?: + | true + | { + name?: string; + icon?: string; //If defined replace original icon url of your app + }; + + //Display app as a standalone application in a tab + tab?: + | { + url: string; + } + | true; + + //Display app as a standalone application on the left bar + standalone?: + | { + url: string; + } + | true; + + //Define where the app can be configured from + configuration?: ("global" | "channel")[]; + }; +}; diff --git a/twake/backend/node/src/services/applications/entities/company-application.ts b/twake/backend/node/src/services/applications/entities/company-application.ts new file mode 100644 index 00000000..7598e05f --- /dev/null +++ b/twake/backend/node/src/services/applications/entities/company-application.ts @@ -0,0 +1,40 @@ +import { Type } from "class-transformer"; +import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators"; +import { PublicApplicationObject } from "./application"; + +export const TYPE = "group_app"; + +@Entity(TYPE, { + primaryKey: [["group_id"], "app_id", "id"], + type: TYPE, +}) +export default class CompanyApplication { + @Type(() => String) + @Column("group_id", "timeuuid") + company_id: string; + + @Type(() => String) + @Column("app_id", "timeuuid") + application_id: string; + + @Type(() => String) + @Column("id", "timeuuid", { generator: "timeuuid" }) + id: string; + + @Column("created_at", "number") + created_at: number; + + @Type(() => String) + @Column("created_by", "string") + created_by: string; //Will be the default delegated user when doing actions on Twake +} + +export type CompanyApplicationPrimaryKey = Pick< + CompanyApplication, + "company_id" | "application_id" | "id" +>; + +export class CompanyApplicationWithApplication extends CompanyApplication { + //Not in database but attached to this object + application?: PublicApplicationObject; +} diff --git a/twake/backend/node/src/services/applications/index.ts b/twake/backend/node/src/services/applications/index.ts new file mode 100644 index 00000000..7c73c6a8 --- /dev/null +++ b/twake/backend/node/src/services/applications/index.ts @@ -0,0 +1,24 @@ +import { Prefix, TwakeService } from "../../core/platform/framework"; +import WebServerAPI from "../../core/platform/services/webserver/provider"; +import web from "./web"; + +@Prefix("/internal/services/applications/v1") +export default class ApplicationsService extends TwakeService { + version = "1"; + name = "applications"; + + public async doInit(): Promise { + const fastify = this.context.getProvider("webserver").getServer(); + fastify.register((instance, _opts, next) => { + web(instance, { prefix: this.prefix }); + next(); + }); + + return this; + } + + // TODO: remove + api(): undefined { + return undefined; + } +} diff --git a/twake/backend/node/src/services/applications/realtime.ts b/twake/backend/node/src/services/applications/realtime.ts new file mode 100644 index 00000000..074f8a3e --- /dev/null +++ b/twake/backend/node/src/services/applications/realtime.ts @@ -0,0 +1,13 @@ +import { WebsocketMetadata } from "../../utils/types"; + +export function getCompanyApplicationRooms(companyId: string): WebsocketMetadata[] { + return [ + { + room: getCompanyApplicationRoom(companyId), + }, + ]; +} + +export function getCompanyApplicationRoom(companyApplicationId: string): string { + return `/company-application/${companyApplicationId}`; +} diff --git a/twake/backend/node/src/services/applications/services/applications.ts b/twake/backend/node/src/services/applications/services/applications.ts new file mode 100644 index 00000000..4eecdd8f --- /dev/null +++ b/twake/backend/node/src/services/applications/services/applications.ts @@ -0,0 +1,143 @@ +import Application, { + ApplicationPrimaryKey, + getInstance as getApplicationInstance, + PublicApplicationObject, + TYPE, +} from "../entities/application"; +import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; +import { Initializable, logger, TwakeServiceProvider } from "../../../core/platform/framework"; +import { + DeleteResult, + ExecutionContext, + ListResult, + OperationType, + Pagination, + SaveResult, +} from "../../../core/platform/framework/api/crud-service"; +import SearchRepository from "../../../core/platform/services/search/repository"; +import assert from "assert"; + +import gr from "../../global-resolver"; +import { InternalToHooksProcessor } from "./internal-event-to-hooks"; + +export class ApplicationServiceImpl implements TwakeServiceProvider, Initializable { + version: "1"; + repository: Repository; + searchRepository: SearchRepository; + + async init(): Promise { + try { + this.searchRepository = gr.platformServices.search.getRepository( + TYPE, + Application, + ); + this.repository = await gr.database.getRepository(TYPE, Application); + } catch (err) { + console.log(err); + logger.error("Error while initializing applications service"); + } + + gr.platformServices.messageQueue.processor.addHandler(new InternalToHooksProcessor()); + + return this; + } + + async get(pk: ApplicationPrimaryKey, context: ExecutionContext): Promise { + return await this.repository.findOne(pk, {}, context); + } + + async list( + pagination: Pagination, + options?: { search?: string }, + context?: ExecutionContext, + ): Promise> { + let entities: ListResult; + if (options.search) { + entities = await this.searchRepository.search( + {}, + { + pagination, + $text: { + $search: options.search, + }, + }, + context, + ); + } else { + entities = await this.repository.find({}, { pagination }, context); + } + entities.filterEntities(app => app.publication.published); + + const applications = entities + .getEntities() + .filter(app => app) + .map(app => app.getPublicObject()); + return new ListResult(entities.type, applications, entities.nextPage); + } + + async listUnpublished(context: ExecutionContext): Promise { + const entities = await this.repository.find({}, {}, context); + entities.filterEntities(app => !app.publication.published); + return entities.getEntities(); + } + + async listDefaults(context: ExecutionContext): Promise> { + const entities = []; + + let page: Pagination = { limitStr: "100" }; + do { + const applicationListResult = await this.repository.find({}, { pagination: page }, context); + page = applicationListResult.nextPage as Pagination; + applicationListResult.filterEntities(app => app.publication.published && app.is_default); + + for (const application of applicationListResult.getEntities()) { + if (application) entities.push(application.getPublicObject()); + } + } while (page.page_token); + + return new ListResult(TYPE, entities); + } + + async save( + item: Application, + options?: SaveOptions, + context?: ExecutionContext, + ): Promise> { + assert(item.company_id, "company_id is not defined"); + + try { + const entity = getApplicationInstance(item); + await this.repository.save(entity, context); + return new SaveResult("application", entity, OperationType.UPDATE); + } catch (e) { + throw e; + } + } + + async delete( + pk: ApplicationPrimaryKey, + context?: ExecutionContext, + ): Promise> { + const entity = await this.get(pk, context); + await this.repository.remove(entity, context); + return new DeleteResult("application", entity, true); + } + + async publish(pk: ApplicationPrimaryKey, context: ExecutionContext): Promise { + const entity = await this.get(pk, context); + if (!entity) { + throw new Error("Entity not found"); + } + entity.publication.published = true; + await this.repository.save(entity, context); + } + + async unpublish(pk: ApplicationPrimaryKey, context: ExecutionContext): Promise { + const entity = await this.get(pk, context); + if (!entity) { + throw new Error("Entity not found"); + } + entity.publication.published = false; + await this.repository.save(entity, context); + } +} diff --git a/twake/backend/node/src/services/applications/services/company-applications.ts b/twake/backend/node/src/services/applications/services/company-applications.ts new file mode 100644 index 00000000..5ae82621 --- /dev/null +++ b/twake/backend/node/src/services/applications/services/company-applications.ts @@ -0,0 +1,188 @@ +import CompanyApplication, { + CompanyApplicationPrimaryKey, + CompanyApplicationWithApplication, + TYPE, +} from "../entities/company-application"; +import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; +import { + Initializable, + logger, + RealtimeDeleted, + RealtimeSaved, + TwakeServiceProvider, +} from "../../../core/platform/framework"; +import { + DeleteResult, + ListResult, + OperationType, + Paginable, + Pagination, + SaveResult, +} from "../../../core/platform/framework/api/crud-service"; +import { CompanyExecutionContext } from "../web/types"; +import { getCompanyApplicationRoom } from "../realtime"; +import gr from "../../global-resolver"; + +export class CompanyApplicationServiceImpl implements TwakeServiceProvider, Initializable { + version: "1"; + repository: Repository; + + async init(): Promise { + try { + this.repository = await gr.database.getRepository( + TYPE, + CompanyApplication, + ); + } catch (err) { + console.log(err); + logger.error("Error while initializing applications service"); + } + return this; + } + + // TODO: remove logic from context + async get( + pk: Pick & { id?: string }, + context?: CompanyExecutionContext, + ): Promise { + const companyApplication = await this.repository.findOne( + { + group_id: context ? context.company.id : pk.company_id, + app_id: pk.application_id, + }, + {}, + context, + ); + + const application = await gr.services.applications.marketplaceApps.get( + { + id: pk.application_id, + }, + context, + ); + + return { + ...companyApplication, + application: application, + }; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @RealtimeSaved((companyApplication, _context) => { + return [ + { + room: getCompanyApplicationRoom(companyApplication.id), + resource: companyApplication, + }, + ]; + }) + async save( + item: Pick, + _?: SaveOptions, + context?: CompanyExecutionContext, + ): Promise> { + if (!context?.user?.id && !context?.user?.server_request) { + throw new Error("Only an user of a company can add an application to a company."); + } + + let operation = OperationType.UPDATE; + let companyApplication = await this.repository.findOne( + { + group_id: context?.company.id, + app_id: item.application_id, + }, + {}, + context, + ); + if (!companyApplication) { + operation = OperationType.CREATE; + + companyApplication = new CompanyApplication(); + companyApplication.company_id = context.company.id; + companyApplication.application_id = item.application_id; + companyApplication.created_at = new Date().getTime(); + companyApplication.created_by = context?.user?.id || ""; + + await this.repository.save(companyApplication, context); + } + + return new SaveResult(TYPE, companyApplication, operation); + } + + async initWithDefaultApplications( + companyId: string, + context: CompanyExecutionContext, + ): Promise { + const defaultApps = await gr.services.applications.marketplaceApps.listDefaults(context); + for (const defaultApp of defaultApps.getEntities()) { + await this.save({ company_id: companyId, application_id: defaultApp.id }, {}, context); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @RealtimeDeleted((companyApplication, _context) => { + return [ + { + room: getCompanyApplicationRoom(companyApplication.id), + resource: companyApplication, + }, + ]; + }) + async delete( + pk: CompanyApplicationPrimaryKey, + context?: CompanyExecutionContext, + ): Promise> { + const companyApplication = await this.repository.findOne( + { + group_id: context.company.id, + app_id: pk.application_id, + }, + {}, + context, + ); + + let deleted = false; + if (companyApplication) { + this.repository.remove(companyApplication, context); + deleted = true; + } + + return new DeleteResult(TYPE, companyApplication, deleted); + } + + async list( + pagination: Paginable, + options?: ListOptions, + context?: CompanyExecutionContext, + ): Promise> { + const companyApplications = await this.repository.find( + { + group_id: context.company.id, + }, + { pagination: Pagination.fromPaginable(pagination) }, + context, + ); + + const applications = []; + + for (const companyApplication of companyApplications.getEntities()) { + const application = await gr.services.applications.marketplaceApps.get( + { + id: companyApplication.application_id, + }, + context, + ); + if (application) + applications.push({ + ...companyApplication, + application: application, + }); + } + + return new ListResult( + TYPE, + applications, + companyApplications.nextPage, + ); + } +} diff --git a/twake/backend/node/src/services/applications/services/hooks.ts b/twake/backend/node/src/services/applications/services/hooks.ts new file mode 100644 index 00000000..d34a5f15 --- /dev/null +++ b/twake/backend/node/src/services/applications/services/hooks.ts @@ -0,0 +1,96 @@ +import Application from "../entities/application"; +import Repository from "../../../core/platform/services/database/services/orm/repository/repository"; +import { + Initializable, + logger as log, + TwakeServiceProvider, +} from "../../../core/platform/framework"; +import { CrudException, ExecutionContext } from "../../../core/platform/framework/api/crud-service"; +import SearchRepository from "../../../core/platform/services/search/repository"; +import axios from "axios"; +import * as crypto from "crypto"; +import { isObject } from "lodash"; +import gr from "../../global-resolver"; + +export class ApplicationHooksService implements TwakeServiceProvider, Initializable { + version: "1"; + repository: Repository; + searchRepository: SearchRepository; + + async init() { + return this; + } + + async notifyApp( + application_id: string, + connection_id: string, + user_id: string, + type: string, + name: string, + content: any, + company_id: string, + workspace_id: string, + context: ExecutionContext, + ): Promise { + const app = await gr.services.applications.marketplaceApps.get({ id: application_id }, context); + if (!app) { + throw CrudException.notFound("Application not found"); + } + + if (!app.api.hooks_url) { + throw CrudException.badRequest("Application hooks_url is not defined"); + } + + const payload = { + type, + name, + content, + connection_id: connection_id, + user_id: user_id, + company_id, + workspace_id, + }; + + const signature = crypto + .createHmac("sha256", app.api.private_key) + .update(JSON.stringify(payload)) + .digest("hex"); + + return await axios + .post(app.api.hooks_url, payload, { + headers: { + "Content-Type": "application/json", + "X-Twake-Signature": signature, + }, + }) + + .then(({ data }) => data) + .catch(e => { + log.error(e.message); + const r = e.response; + + if (!r) { + throw CrudException.badGateway("Can't connect remote application"); + } + + let msg = r.data; + + if (isObject(msg)) { + // parse typical responses + if (r.data.message) { + msg = r.data.message; + } else if (r.data.error) { + msg = r.data.error; + } else { + msg = JSON.stringify(r.data); + } + } + + if (r.status == 403) { + throw CrudException.forbidden(msg); + } else { + throw CrudException.badRequest(msg); + } + }); + } +} diff --git a/twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts b/twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts new file mode 100644 index 00000000..77fe8d74 --- /dev/null +++ b/twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts @@ -0,0 +1,64 @@ +import { logger } from "../../../core/platform/framework"; +import { HookType } from "../../applications-api/types"; +import { MessageQueueHandler } from "../../../core/platform/services/message-queue/api"; +import { MessageHook } from "../../messages/types"; +import gr from "../../global-resolver"; +import { ExecutionContext } from "../../../core/platform/framework/api/crud-service"; + +export class InternalToHooksProcessor implements MessageQueueHandler { + readonly topics = { + in: "application:hook:message", + }; + + readonly options = { + unique: true, + ack: true, + queue: "application:hook:message:consumer1", + }; + + readonly name = "Application::InternalToHooksProcessor"; + + validate(_: HookType): boolean { + return true; + } + + async process(message: HookType, context?: ExecutionContext): Promise { + logger.debug(`${this.name} - Receive hook of type ${message.type}`); + + const application = await gr.services.applications.marketplaceApps.get( + { + id: message.application_id, + }, + context, + ); + + //TODO Check application access rights (hooks) + const _access = application.access; + + // Check application still exists in the company + if ( + !(await gr.services.applications.companyApps.get({ + company_id: message.company_id, + application_id: message.application_id, + id: undefined, + })) + ) { + logger.error( + `${this.name} - Application ${message.application_id} not found in company ${message.company_id}`, + ); + return; + } + + await gr.services.applications.hooks.notifyApp( + message.application_id, + null, + null, + "hook", + null, + { message }, + null, + null, + context, + ); + } +} diff --git a/twake/backend/node/src/services/applications/web/controllers/applications.ts b/twake/backend/node/src/services/applications/web/controllers/applications.ts new file mode 100644 index 00000000..11cbaa2d --- /dev/null +++ b/twake/backend/node/src/services/applications/web/controllers/applications.ts @@ -0,0 +1,296 @@ +import { FastifyReply, FastifyRequest } from "fastify"; +import { CrudController } from "../../../../core/platform/services/webserver/types"; +import { + PaginationQueryParameters, + ResourceCreateResponse, + ResourceDeleteResponse, + ResourceGetResponse, + ResourceListResponse, + ResourceUpdateResponse, +} from "../../../../utils/types"; +import Application, { + ApplicationObject, + PublicApplicationObject, +} from "../../entities/application"; +import { + CrudException, + ExecutionContext, + Pagination, +} from "../../../../core/platform/framework/api/crud-service"; +import _ from "lodash"; +import { randomBytes } from "crypto"; +import { ApplicationEventRequestBody } from "../types"; +import { logger as log } from "../../../../core/platform/framework"; +import { hasCompanyAdminLevel } from "../../../../utils/company"; +import gr from "../../../global-resolver"; +import config from "../../../../core/config"; +import axios from "axios"; + +export class ApplicationController + implements + CrudController< + ResourceGetResponse, + ResourceUpdateResponse, + ResourceListResponse, + ResourceDeleteResponse + > +{ + async get( + request: FastifyRequest<{ Params: { application_id: string } }>, + ): Promise> { + const context = getExecutionContext(request); + + const entity = await gr.services.applications.marketplaceApps.get( + { + id: request.params.application_id, + }, + context, + ); + + const companyUser = await gr.services.companies.getCompanyUser( + { id: entity.company_id }, + { id: context.user.id }, + ); + + const isAdmin = companyUser && companyUser.role == "admin"; + + return { + resource: isAdmin ? entity.getApplicationObject() : entity.getPublicObject(), + }; + } + + async list( + request: FastifyRequest<{ + Querystring: PaginationQueryParameters & { search: string }; + }>, + ): Promise> { + const entities = await gr.services.applications.marketplaceApps.list(new Pagination(), { + search: request.query.search, + }); + return { + resources: entities.getEntities(), + next_page_token: entities.nextPage.page_token, + }; + } + + async save( + request: FastifyRequest<{ + Params: { application_id: string }; + Body: { resource: Application }; + }>, + _reply: FastifyReply, + ): Promise> { + const context = getExecutionContext(request); + + try { + const app = request.body.resource; + const now = new Date().getTime(); + const pluginsEndpoint = config.get("plugins.api"); + + let entity: Application; + + if (request.params.application_id) { + entity = await gr.services.applications.marketplaceApps.get( + { + id: request.params.application_id, + }, + context, + ); + + if (!entity) { + throw CrudException.notFound("Application not found"); + } + + entity.publication.requested = app.publication.requested; + if (app.publication.requested === false) { + entity.publication.published = false; + } + + if (entity.publication.published) { + if ( + !_.isEqual( + _.pick(entity, "identity", "api", "access", "display"), + _.pick(app, "identity", "api", "access", "display"), + ) + ) { + throw CrudException.badRequest( + "You can't update applications details while it published", + ); + } + } + + entity.identity = app.identity; + entity.api.hooks_url = app.api.hooks_url; + entity.api.allowed_ips = app.api.allowed_ips; + entity.access = app.access; + entity.display = app.display; + + entity.stats.updated_at = now; + entity.stats.version++; + + const res = await gr.services.applications.marketplaceApps.save(entity); + entity = res.entity; + } else { + // INSERT + + app.is_default = false; + app.publication.published = false; + app.api.private_key = randomBytes(32).toString("base64"); + + app.stats = { + created_at: now, + updated_at: now, + version: 0, + }; + + const res = await gr.services.applications.marketplaceApps.save(app); + entity = res.entity; + } + + // SYNC PLUGINS + if (app.identity.repository) { + try { + axios + .post( + `${pluginsEndpoint}/add`, + { + gitRepo: app.identity.repository, + pluginId: entity.getApplicationObject().id, + pluginSecret: entity.getApplicationObject().api.private_key, + }, + { + headers: { + "Content-Type": "application/json", + }, + }, + ) + .then(response => { + log.info(response.data); + }) + .catch(error => { + log.error(error); + }); + } catch (error) { + console.error(error); + } + } + + return { + resource: entity.getApplicationObject(), + }; + } catch (e) { + log.error(e); + throw e; + } + } + + async delete( + request: FastifyRequest<{ Params: { application_id: string } }>, + reply: FastifyReply, + ): Promise { + const context = getExecutionContext(request); + + const application = await gr.services.applications.marketplaceApps.get( + { + id: request.params.application_id, + }, + context, + ); + + const compUser = await gr.services.companies.getCompanyUser( + { id: application.company_id }, + { id: context.user.id }, + ); + if (!compUser || !hasCompanyAdminLevel(compUser.role)) { + throw CrudException.forbidden("You don't have the rights to delete this application"); + } + + const deleteResult = await gr.services.applications.marketplaceApps.delete( + { + id: request.params.application_id, + }, + context, + ); + + if (deleteResult.deleted) { + reply.code(204); + + return { + status: "success", + }; + } + + return { + status: "error", + }; + } + + async event( + request: FastifyRequest<{ + Body: ApplicationEventRequestBody; + Params: { application_id: string }; + }>, + _reply: FastifyReply, + ): Promise> { + const context = getExecutionContext(request); + + const content = request.body.data; + + const applicationEntity = await gr.services.applications.marketplaceApps.get( + { + id: request.params.application_id, + }, + context, + ); + + if (!applicationEntity) { + throw CrudException.notFound("Application not found"); + } + + const companyUser = gr.services.companies.getCompanyUser( + { id: request.body.company_id }, + { id: context.user.id }, + ); + + if (!companyUser) { + throw CrudException.badRequest( + "You cannot send event to an application from another company", + ); + } + + const applicationInCompany = await gr.services.applications.companyApps.get({ + company_id: request.body.company_id, + application_id: request.params.application_id, + id: undefined, + }); + + if (!applicationInCompany) { + throw CrudException.badRequest("Application isn't installed in this company"); + } + + const hookResponse = await gr.services.applications.hooks.notifyApp( + request.params.application_id, + request.body.connection_id, + context.user.id, + request.body.type, + request.body.name, + content, + request.body.company_id, + request.body.workspace_id, + context, + ); + + return { + resource: hookResponse, + }; + } +} + +function getExecutionContext(request: FastifyRequest): ExecutionContext { + return { + user: request.currentUser, + url: request.url, + method: request.routerMethod, + transport: "http", + }; +} diff --git a/twake/backend/node/src/services/applications/web/controllers/company-applications.ts b/twake/backend/node/src/services/applications/web/controllers/company-applications.ts new file mode 100644 index 00000000..26542e39 --- /dev/null +++ b/twake/backend/node/src/services/applications/web/controllers/company-applications.ts @@ -0,0 +1,119 @@ +import { FastifyReply, FastifyRequest } from "fastify"; + +import { + PaginationQueryParameters, + ResourceDeleteResponse, + ResourceGetResponse, + ResourceListResponse, + ResourceUpdateResponse, +} from "../../../../utils/types"; +import { PublicApplicationObject } from "../../entities/application"; +import { CompanyExecutionContext } from "../types"; +import { CrudController } from "../../../../core/platform/services/webserver/types"; +import { getCompanyApplicationRooms } from "../../realtime"; +import gr from "../../../global-resolver"; + +export class CompanyApplicationController + implements + CrudController< + ResourceGetResponse, + ResourceUpdateResponse, + ResourceListResponse, + ResourceDeleteResponse + > +{ + async get( + request: FastifyRequest<{ Params: { company_id: string; application_id: string } }>, + ): Promise> { + const context = getCompanyExecutionContext(request); + const resource = await gr.services.applications.companyApps.get( + { + application_id: request.params.application_id, + company_id: context.company.id, + id: undefined, + }, + context, + ); + return { + resource: resource?.application, + }; + } + + async list( + request: FastifyRequest<{ + Params: { company_id: string }; + Querystring: PaginationQueryParameters & { search: string }; + }>, + ): Promise> { + const context = getCompanyExecutionContext(request); + const resources = await gr.services.applications.companyApps.list( + request.query, + { search: request.query.search }, + context, + ); + + return { + resources: resources.getEntities().map(ca => ca.application), + next_page_token: resources.nextPage.page_token, + websockets: + gr.platformServices.realtime.sign( + getCompanyApplicationRooms(request.params.company_id), + context.user.id, + ) || [], + }; + } + + async save( + request: FastifyRequest<{ + Params: { company_id: string; application_id: string }; + Body: PublicApplicationObject; + }>, + ): Promise> { + const context = getCompanyExecutionContext(request); + + const resource = await gr.services.applications.companyApps.save( + { application_id: request.params.application_id, company_id: context.company.id }, + {}, + context, + ); + + const app = await gr.services.applications.companyApps.get(resource.entity); + + return { + resource: app.application, + }; + } + + async delete( + request: FastifyRequest<{ Params: { company_id: string; application_id: string } }>, + _reply: FastifyReply, + ): Promise { + const context = getCompanyExecutionContext(request); + const resource = await gr.services.applications.companyApps.delete( + { + application_id: request.params.application_id, + company_id: context.company.id, + id: undefined, + }, + context, + ); + return { + status: resource.deleted ? "success" : "error", + }; + } +} + +function getCompanyExecutionContext( + request: FastifyRequest<{ + Params: { company_id: string }; + }>, +): CompanyExecutionContext { + return { + user: request.currentUser, + company: { id: request.params.company_id }, + url: request.url, + method: request.routerMethod, + reqId: request.id, + transport: "http", + }; +} diff --git a/twake/backend/node/src/services/applications/web/controllers/index.ts b/twake/backend/node/src/services/applications/web/controllers/index.ts new file mode 100644 index 00000000..ef52722d --- /dev/null +++ b/twake/backend/node/src/services/applications/web/controllers/index.ts @@ -0,0 +1 @@ +export * from "./applications"; diff --git a/twake/backend/node/src/services/applications/web/index.ts b/twake/backend/node/src/services/applications/web/index.ts new file mode 100644 index 00000000..3c2df979 --- /dev/null +++ b/twake/backend/node/src/services/applications/web/index.ts @@ -0,0 +1,12 @@ +import { FastifyInstance, FastifyRegisterOptions } from "fastify"; +import routes from "./routes"; + +export default ( + fastify: FastifyInstance, + options: FastifyRegisterOptions<{ + prefix: string; + }>, +): void => { + fastify.log.debug("Configuring /internal/services/applications/v1 routes"); + fastify.register(routes, options); +}; diff --git a/twake/backend/node/src/services/applications/web/routes.ts b/twake/backend/node/src/services/applications/web/routes.ts new file mode 100644 index 00000000..3d5835c7 --- /dev/null +++ b/twake/backend/node/src/services/applications/web/routes.ts @@ -0,0 +1,161 @@ +import { FastifyInstance, FastifyPluginCallback, FastifyRequest } from "fastify"; +import { ApplicationController } from "./controllers/applications"; +import { CompanyApplicationController } from "./controllers/company-applications"; + +import Application from "../entities/application"; +import { applicationEventHookSchema, applicationPostSchema } from "./schemas"; +import { logger as log } from "../../../core/platform/framework"; +import { checkUserBelongsToCompany, hasCompanyAdminLevel } from "../../../utils/company"; +import gr from "../../global-resolver"; + +const applicationsUrl = "/applications"; +const companyApplicationsUrl = "/companies/:company_id/applications"; + +const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) => { + const applicationController = new ApplicationController(); + const companyApplicationController = new CompanyApplicationController(); + + const adminCheck = async ( + request: FastifyRequest<{ + Body: { resource: Application }; + Params: { application_id: string }; + }>, + ) => { + try { + let companyId: string = request.body?.resource?.company_id; + + if (request.params.application_id) { + const application = await gr.services.applications.marketplaceApps.get( + { + id: request.params.application_id, + }, + undefined, + ); + + if (!application) { + throw fastify.httpErrors.notFound("Application is not defined"); + } + + companyId = application.company_id; + } + + const userId = request.currentUser.id; + + if (!companyId) { + throw fastify.httpErrors.forbidden(`Company ${companyId} not found`); + } + + const companyUser = await checkUserBelongsToCompany(userId, companyId); + + if (!hasCompanyAdminLevel(companyUser.role)) { + throw fastify.httpErrors.forbidden("You must be an admin of this company"); + } + } catch (e) { + log.error(e); + throw e; + } + }; + + /** + * Applications collection + * Marketplace of applications + */ + + //Get and search list of applications in the marketplace + fastify.route({ + method: "GET", + url: `${applicationsUrl}`, + preValidation: [fastify.authenticate], + // schema: applicationGetSchema, + handler: applicationController.list.bind(applicationController), + }); + + //Get a single application in the marketplace + fastify.route({ + method: "GET", + url: `${applicationsUrl}/:application_id`, + preValidation: [fastify.authenticate], + // schema: applicationGetSchema, + handler: applicationController.get.bind(applicationController), + }); + + //Create application (must be my company application and I must be company admin) + fastify.route({ + method: "POST", + url: `${applicationsUrl}`, + preHandler: [adminCheck], + preValidation: [fastify.authenticate], + schema: applicationPostSchema, + handler: applicationController.save.bind(applicationController), + }); + + //Edit application (must be my company application and I must be company admin) + fastify.route({ + method: "POST", + url: `${applicationsUrl}/:application_id`, + preHandler: [adminCheck], + preValidation: [fastify.authenticate], + schema: applicationPostSchema, + handler: applicationController.save.bind(applicationController), + }); + + // Delete application (must be my company application and I must be company admin) + fastify.route({ + method: "DELETE", + url: `${applicationsUrl}/:application_id`, + preHandler: [adminCheck], + preValidation: [fastify.authenticate], + handler: applicationController.delete.bind(applicationController), + }); + + /** + * Company applications collection + * Company-wide available applications + * (must be my company application and I must be company admin) + */ + + //Get list of applications for a company + fastify.route({ + method: "GET", + url: `${companyApplicationsUrl}`, + preValidation: [fastify.authenticate], + handler: companyApplicationController.list.bind(companyApplicationController), + }); + + //Get one application of a company + fastify.route({ + method: "GET", + url: `${companyApplicationsUrl}/:application_id`, + preValidation: [fastify.authenticate], + handler: companyApplicationController.get.bind(companyApplicationController), + }); + + //Remove an application from a company + fastify.route({ + method: "DELETE", + url: `${companyApplicationsUrl}/:application_id`, + preValidation: [fastify.authenticate], + handler: companyApplicationController.delete.bind(companyApplicationController), + }); + + //Add an application to the company + fastify.route({ + method: "POST", + url: `${companyApplicationsUrl}/:application_id`, + preValidation: [fastify.authenticate], + handler: companyApplicationController.save.bind(companyApplicationController), + }); + + //Application event triggered by a user + fastify.route({ + method: "POST", + url: `${applicationsUrl}/:application_id/event`, + preValidation: [fastify.authenticate], + schema: applicationEventHookSchema, + handler: applicationController.event.bind(applicationController), + }); + + next(); +}; + +export default routes; diff --git a/twake/backend/node/src/services/applications/web/schemas.ts b/twake/backend/node/src/services/applications/web/schemas.ts new file mode 100644 index 00000000..996cea70 --- /dev/null +++ b/twake/backend/node/src/services/applications/web/schemas.ts @@ -0,0 +1,129 @@ +export const applicationsSchema = { + type: "object", + properties: {}, +}; + +const applicationIdentity = { + type: "object", + properties: { + code: { type: "string" }, + name: { type: "string" }, + icon: { type: "string" }, + description: { type: "string" }, + website: { type: "string" }, + categories: { type: "array", items: { type: "string" } }, + compatibility: { type: "array", items: { type: "string" } }, + }, + required: ["code", "name", "icon", "description", "website", "categories", "compatibility"], +}; + +const applicationAccess = { + type: "object", + properties: { + read: { type: "array", items: { type: "string" } }, + write: { type: "array", items: { type: "string" } }, + delete: { type: "array", items: { type: "string" } }, + hooks: { type: "array", items: { type: "string" } }, + }, + required: ["read", "write", "delete", "hooks"], +}; + +const requestApplicationPublication = { + type: "object", + properties: { + requested: { type: "boolean" }, + }, + required: ["requested"], +}; + +const responseApplicationPublication = { + type: "object", + properties: { + published: { type: "boolean" }, + requested: { type: "boolean" }, + }, + required: ["requested", "published"], +}; + +const applicationStats = { + type: "object", + properties: { + created_at: { type: "number" }, + updated_at: { type: "number" }, + version: { type: "number" }, + }, + required: ["created_at", "updated_at", "version"], +}; + +const apiObject = { + type: "object", + properties: { + hooks_url: { type: "string" }, + allowed_ips: { type: "string" }, + private_key: { type: "string" }, + }, + required: ["hooks_url", "allowed_ips"], +}; + +const requestApplicationObject = { + type: "object", + properties: { + company_id: { type: "string" }, + identity: applicationIdentity, + access: applicationAccess, + display: {}, + api: apiObject, + publication: requestApplicationPublication, + }, + required: ["company_id", "identity", "access", "display", "api", "publication"], + additionalProperties: false, +}; + +const responseApplicationObject = { + type: "object", + properties: { + id: { type: "string" }, + is_default: { type: "boolean" }, + company_id: { type: "string" }, + identity: applicationIdentity, + access: applicationAccess, + display: {}, + publication: responseApplicationPublication, + api: apiObject, + stats: applicationStats, + }, + required: [ + "id", + "is_default", + "company_id", + "identity", + "access", + "display", + "publication", + "stats", + ], + additionalProperties: false, +}; + +export const applicationPostSchema = { + body: { type: "object", properties: { resource: requestApplicationObject } }, + response: { + "2xx": { + resource: responseApplicationObject, + }, + }, +}; + +export const applicationEventHookSchema = { + body: { + type: "object", + properties: { + company_id: { type: "string" }, + workspace_id: { type: "string" }, + type: { type: "string" }, + name: { type: "string" }, + content: {}, + }, + required: ["company_id", "workspace_id", "type", "content"], + }, +}; diff --git a/twake/backend/node/src/services/applications/web/types.ts b/twake/backend/node/src/services/applications/web/types.ts new file mode 100644 index 00000000..5a60f3b3 --- /dev/null +++ b/twake/backend/node/src/services/applications/web/types.ts @@ -0,0 +1,15 @@ +import { ExecutionContext } from "../../../core/platform/framework/api/crud-service"; + +export interface CompanyExecutionContext extends ExecutionContext { + company: { id: string }; +} + +export interface ApplicationEventRequestBody { + company_id: string; + workspace_id: string; + connection_id: string; + type: string; + name?: string; + content: any; + data: any; +} diff --git a/twake/backend/node/test/e2e/documents/assets/test.txt b/twake/backend/node/test/e2e/documents/assets/test.txt new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/twake/backend/node/test/e2e/documents/assets/test.txt @@ -0,0 +1 @@ +test diff --git a/twake/backend/node/test/e2e/documents/documents-tab.spec.ts b/twake/backend/node/test/e2e/documents/documents-tab.spec.ts new file mode 100644 index 00000000..e9d6c84e --- /dev/null +++ b/twake/backend/node/test/e2e/documents/documents-tab.spec.ts @@ -0,0 +1,183 @@ +import { afterAll, afterEach, beforeEach, describe, expect, it } from "@jest/globals"; +import { deserialize } from "class-transformer"; +import { AccessInformation } from "../../../src/services/documents/entities/drive-file"; +import { init, TestPlatform } from "../setup"; +import { TestDbService } from "../utils.prepare.db"; +import { e2e_createDocument, e2e_getDocument } from "./utils"; + +const url = "/internal/services/documents/v1"; + +describe("the Drive Twake tabs feature", () => { + let platform: TestPlatform; + + class DriveFileMockClass { + id: string; + name: string; + size: number; + added: string; + parent_id: string; + access_info: AccessInformation; + } + + class DriveItemDetailsMockClass { + path: string[]; + item: DriveFileMockClass; + children: DriveFileMockClass[]; + versions: Record[]; + } + + beforeEach(async () => { + platform = await init({ + services: [ + "webserver", + "database", + "applications", + "search", + "storage", + "message-queue", + "user", + "search", + "files", + "websocket", + "messages", + "auth", + "realtime", + "channels", + "counter", + "statistics", + "platform-services", + "documents", + ], + }); + }); + + afterEach(async () => { + await platform.tearDown(); + }); + + afterAll(async () => { + await platform.app.close(); + }); + + it("did create a tab configuration on Drive side", async done => { + await TestDbService.getInstance(platform, true); + + const item = { + name: "new tab test file", + parent_id: "root", + company_id: platform.workspace.company_id, + }; + + const version = {}; + + const response = await e2e_createDocument(platform, item, version); + const doc = deserialize(DriveFileMockClass, response.body); + + const tab = { + company_id: platform.workspace.company_id, + tab_id: "1234567890", + channel_id: "abcdefghij", + item_id: doc.id, + level: "write", + }; + + const token = await platform.auth.getJWTToken(); + + const createdTab = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/tabs/${tab.tab_id}`, + headers: { + authorization: `Bearer ${token}`, + }, + payload: tab, + }); + + expect(createdTab.statusCode).toBe(200); + expect(createdTab.body).toBeDefined(); + expect(createdTab.json().company_id).toBe(tab.company_id); + expect(createdTab.json().tab_id).toBe(tab.tab_id); + expect(createdTab.json().item_id).toBe(tab.item_id); + + const getTabResponse = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/tabs/${tab.tab_id}`, + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(getTabResponse.statusCode).toBe(200); + expect(getTabResponse.body).toBeDefined(); + expect(getTabResponse.json().company_id).toBe(tab.company_id); + expect(getTabResponse.json().tab_id).toBe(tab.tab_id); + expect(getTabResponse.json().item_id).toBe(tab.item_id); + + const documentResponse = await e2e_getDocument(platform, doc.id); + const documentResult = deserialize( + DriveItemDetailsMockClass, + documentResponse.body, + ); + + console.log(documentResult?.item); + + expect( + documentResult?.item?.access_info?.entities?.find( + a => a?.type === "channel" && a.id === "abcdefghij" && a.level === "write", + ), + ).toBeDefined(); + + done?.(); + }); + + it("did refuse to create a tab configuration for an item I can't manage", async done => { + const dbService = await TestDbService.getInstance(platform, true); + const ws0pk = { + id: platform.workspace.workspace_id, + company_id: platform.workspace.company_id, + }; + const otherUser = await dbService.createUser([ws0pk]); + + const item = { + name: "new tab test file", + parent_id: "root", + company_id: platform.workspace.company_id, + access_info: { + entities: [ + { + type: "folder", + id: "parent", + level: "none", + } as any, + ], + }, + }; + + const version = {}; + + const response = await e2e_createDocument(platform, item, version); + const doc = deserialize(DriveFileMockClass, response.body); + + const tab = { + company_id: platform.workspace.company_id, + tab_id: "1234567890", + channel_id: "abcdefghij", + item_id: doc.id, + level: "read", + }; + + const token = await platform.auth.getJWTToken({ sub: otherUser.id }); + + const createdTab = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/tabs/${tab.tab_id}`, + headers: { + authorization: `Bearer ${token}`, + }, + payload: tab, + }); + + expect(createdTab.statusCode).toBe(403); + + done?.(); + }); +}); diff --git a/twake/backend/node/test/e2e/documents/documents.spec.ts b/twake/backend/node/test/e2e/documents/documents.spec.ts new file mode 100644 index 00000000..d964740e --- /dev/null +++ b/twake/backend/node/test/e2e/documents/documents.spec.ts @@ -0,0 +1,238 @@ +import { describe, beforeEach, afterEach, it, expect, afterAll } from "@jest/globals"; +import { deserialize } from "class-transformer"; +import { File } from "../../../src/services/files/entities/file"; +import { ResourceUpdateResponse } from "../../../src/utils/types"; +import { init, TestPlatform } from "../setup"; +import { TestDbService } from "../utils.prepare.db"; +import { + e2e_createDocument, + e2e_createDocumentFile, + e2e_createVersion, + e2e_deleteDocument, + e2e_getDocument, + e2e_searchDocument, + e2e_updateDocument, +} from "./utils"; + +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[]; + } + + class SearchResultMockClass { + entities: DriveFileMockClass[]; + } + + beforeEach(async () => { + platform = await init({ + services: [ + "webserver", + "database", + "applications", + "search", + "storage", + "message-queue", + "user", + "search", + "files", + "websocket", + "messages", + "auth", + "realtime", + "channels", + "counter", + "statistics", + "platform-services", + "documents", + ], + }); + }); + + afterEach(async () => { + await platform.tearDown(); + }); + + afterAll(async () => { + await platform.app.close(); + }); + + const createItem = async (): Promise => { + await TestDbService.getInstance(platform, true); + + const item = { + name: "new test file", + parent_id: "root", + company_id: platform.workspace.company_id, + }; + + const version = {}; + + const response = await e2e_createDocument(platform, item, version); + return deserialize(DriveFileMockClass, response.body); + }; + + it("did create the drive item", async done => { + const result = await createItem(); + + expect(result).toBeDefined(); + expect(result.name).toEqual("new test file"); + expect(result.added).toBeDefined(); + + done?.(); + }); + + it("did fetch the drive item", async done => { + await TestDbService.getInstance(platform, true); + + const response = await e2e_getDocument(platform, ""); + const result = deserialize(DriveItemDetailsMockClass, response.body); + + expect(result.item.name).toEqual("root"); + + done?.(); + }); + + it("did fetch the trash", async done => { + await TestDbService.getInstance(platform, true); + + const response = await e2e_getDocument(platform, "trash"); + const result = deserialize(DriveItemDetailsMockClass, response.body); + + expect(result.item.name).toEqual("trash"); + + done?.(); + }); + + it("did delete an item", async done => { + const createItemResult = await createItem(); + + expect(createItemResult.id).toBeDefined(); + + const deleteResponse = await e2e_deleteDocument(platform, createItemResult.id); + expect(deleteResponse.statusCode).toEqual(200); + + done?.(); + }); + + it("did update an item", async done => { + const createItemResult = await createItem(); + + expect(createItemResult.id).toBeDefined(); + + const update = { + name: "somethingelse", + }; + + const updateItemResponse = await e2e_updateDocument(platform, createItemResult.id, update); + const updateItemResult = deserialize( + DriveFileMockClass, + updateItemResponse.body, + ); + + expect(createItemResult.id).toEqual(updateItemResult.id); + expect(updateItemResult.name).toEqual("somethingelse"); + + done?.(); + }); + + it("did move an item to trash", async done => { + const createItemResult = await createItem(); + + expect(createItemResult.id).toBeDefined(); + + const moveToTrashResponse = await e2e_deleteDocument(platform, createItemResult.id); + expect(moveToTrashResponse.statusCode).toEqual(200); + + const listTrashResponse = await e2e_getDocument(platform, "trash"); + const listTrashResult = deserialize( + DriveItemDetailsMockClass, + listTrashResponse.body, + ); + + expect(listTrashResult.item.name).toEqual("trash"); + expect(listTrashResult.children.some(({ id }) => id === createItemResult.id)).toBeTruthy(); + + done?.(); + }); + + // TODO: wait for elastic search index + it("did search for an item", async done => { + const createItemResult = await createItem(); + + expect(createItemResult.id).toBeDefined(); + + await e2e_getDocument(platform, "root"); + await e2e_getDocument(platform, createItemResult.id); + + await new Promise(resolve => setTimeout(resolve, 3000)); + + const searchPayload = { + search: "test", + }; + + const searchResponse = await e2e_searchDocument(platform, searchPayload); + const searchResult = deserialize( + SearchResultMockClass, + searchResponse.body, + ); + + expect(searchResult.entities.length).toBeGreaterThanOrEqual(1); + + done?.(); + }); + + it("did search for an item that doesn't exist", async done => { + await createItem(); + + const unexistingSeachPayload = { + search: "somethingthatdoesn'tandshouldn'texist", + }; + const failSearchResponse = await e2e_searchDocument(platform, unexistingSeachPayload); + const failSearchResult = deserialize( + SearchResultMockClass, + failSearchResponse.body, + ); + + expect(failSearchResult.entities).toHaveLength(0); + + done?.(); + }); + + it("did create a version for a drive item", async done => { + const item = await createItem(); + const fileUploadResponse = await e2e_createDocumentFile(platform); + const fileUploadResult = deserialize>( + ResourceUpdateResponse, + fileUploadResponse.body, + ); + + const file_metadata = { external_id: fileUploadResult.resource.id }; + + await e2e_createVersion(platform, item.id, { filename: "file2", file_metadata }); + await e2e_createVersion(platform, item.id, { filename: "file3", file_metadata }); + await e2e_createVersion(platform, item.id, { filename: "file4", file_metadata }); + + const fetchItemResponse = await e2e_getDocument(platform, item.id); + const fetchItemResult = deserialize( + DriveItemDetailsMockClass, + fetchItemResponse.body, + ); + + expect(fetchItemResult.versions).toHaveLength(4); + + done?.(); + }); +}); diff --git a/twake/backend/node/test/e2e/documents/utils.ts b/twake/backend/node/test/e2e/documents/utils.ts new file mode 100644 index 00000000..1104441c --- /dev/null +++ b/twake/backend/node/test/e2e/documents/utils.ts @@ -0,0 +1,116 @@ +import { DriveFile } from "../../../src/services/documents/entities/drive-file"; +import { FileVersion } from "../../../src/services/documents/entities/file-version"; +import { TestPlatform } from "../setup"; +import formAutoContent from "form-auto-content"; +import fs from "fs"; + +const url = "/internal/services/documents/v1"; + +export const e2e_createDocument = async ( + platform: TestPlatform, + item: Partial, + version: Partial, +) => { + const token = await platform.auth.getJWTToken(); + + return await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/item`, + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + item, + version, + }, + }); +}; + +export const e2e_getDocument = async (platform: TestPlatform, id: string | "root" | "trash") => { + const token = await platform.auth.getJWTToken(); + + return await platform.app.inject({ + method: "GET", + url: `${url}/companies/${platform.workspace.company_id}/item/${id}`, + headers: { + authorization: `Bearer ${token}`, + }, + }); +}; + +export const e2e_deleteDocument = async (platform: TestPlatform, id: string | "root" | "trash") => { + const token = await platform.auth.getJWTToken(); + + return await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${platform.workspace.company_id}/item/${id}`, + headers: { + authorization: `Bearer ${token}`, + }, + }); +}; + +export const e2e_updateDocument = async ( + platform: TestPlatform, + id: string | "root" | "trash", + item: Partial, +) => { + const token = await platform.auth.getJWTToken(); + + return await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/item/${id}`, + headers: { + authorization: `Bearer ${token}`, + }, + payload: item, + }); +}; + +export const e2e_searchDocument = async ( + platform: TestPlatform, + payload: Record, +) => { + const token = await platform.auth.getJWTToken(); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/search`, + headers: { + authorization: `Bearer ${token}`, + }, + payload, + }); + + return response; +}; + +export const e2e_createVersion = async ( + platform: TestPlatform, + id: string, + payload: Partial, +) => { + const token = await platform.auth.getJWTToken(); + + return await platform.app.inject({ + method: "POST", + url: `${url}/companies/${platform.workspace.company_id}/item/${id}/version`, + headers: { + authorization: `Bearer ${token}`, + }, + payload, + }); +}; + +export const e2e_createDocumentFile = async (platform: TestPlatform) => { + const filePath = `${__dirname}/assets/test.txt`; + const token = await platform.auth.getJWTToken(); + const form = formAutoContent({ file: fs.createReadStream(filePath) }); + form.headers["authorization"] = `Bearer ${token}`; + + return await platform.app.inject({ + method: "POST", + url: `/internal/services/files/v1/companies/${platform.workspace.company_id}/files`, + ...form, + }); +}; diff --git a/twake/backend/node/test/e2e/workspaces/workspace-users.spec.ts b/twake/backend/node/test/e2e/workspaces/workspace-users.spec.ts new file mode 100644 index 00000000..c70f2e83 --- /dev/null +++ b/twake/backend/node/test/e2e/workspaces/workspace-users.spec.ts @@ -0,0 +1,469 @@ +import { afterAll, beforeAll, describe, expect, it } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import { TestDbService, uuid } from "../utils.prepare.db"; +import { v1 as uuidv1 } from "uuid"; + +describe("The /workspace users API", () => { + const url = "/internal/services/workspaces/v1"; + let platform: TestPlatform; + + let testDbService: TestDbService; + + const nonExistentId = uuidv1(); + let companyId = ""; + + const checkUserObject = (resource: any) => { + expect(resource).toMatchObject({ + id: expect.any(String), + company_id: expect.any(String), + workspace_id: expect.any(String), + user_id: expect.any(String), + created_at: expect.any(Number), + role: expect.stringMatching(/moderator|member/), + user: { + id: expect.any(String), + provider: expect.any(String), + provider_id: expect.any(String), + email: expect.any(String), + is_verified: expect.any(Boolean), + picture: expect.any(String), + first_name: expect.any(String), + last_name: expect.any(String), + created_at: expect.any(Number), + deleted: expect.any(Boolean), + status: expect.any(String), + last_activity: expect.any(Number), + }, + }); + }; + + beforeAll(async ends => { + platform = await init({ + services: [ + "database", + "message-queue", + "search", + "webserver", + "user", + "workspaces", + "applications", + "auth", + "console", + "counter", + "storage", + "statistics", + "platform-services", + ], + }); + + companyId = platform.workspace.company_id; + + await platform.database.getConnector().init(); + testDbService = new TestDbService(platform); + await testDbService.createCompany(companyId); + const ws0pk = { id: uuidv1(), company_id: companyId }; + const ws1pk = { id: uuidv1(), company_id: companyId }; + const ws2pk = { id: uuidv1(), company_id: companyId }; + const ws3pk = { id: uuidv1(), company_id: companyId }; + await testDbService.createWorkspace(ws0pk); + await testDbService.createWorkspace(ws1pk); + await testDbService.createWorkspace(ws2pk); + await testDbService.createWorkspace(ws3pk); + await testDbService.createUser([ws0pk, ws1pk]); + await testDbService.createUser([ws2pk], { companyRole: "admin" }); + await testDbService.createUser([ws2pk], { workspaceRole: "moderator" }); + await testDbService.createUser([ws2pk], { workspaceRole: "member" }); + await testDbService.createUser([ws2pk], { workspaceRole: "member" }); + await testDbService.createUser([], { companyRole: "member" }); + await testDbService.createUser([ws3pk], { companyRole: "guest", workspaceRole: "member" }); + ends(); + }); + + afterAll(async ends => { + await platform.tearDown(); + ends(); + }); + + describe("The GET /workspaces/users route", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}/users`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 404 when workspace not found", async done => { + const userId = testDbService.users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}/users`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(404); + done(); + }); + + it("should 200 when ok", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(200); + + const resources = response.json()["resources"]; + + expect(resources.length).toBeGreaterThan(0); + + for (const resource of resources) { + checkUserObject(resource); + } + + done(); + }); + }); + + describe("The GET /workspaces/users/:user_id route", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + const userId = testDbService.users[0].id; + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}/users/${userId}`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 404 when workspace not found", async done => { + const userId = testDbService.users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}/users/${userId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(404); + done(); + }); + + it("should 200 when ok", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[0].workspace.id; + const userId = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users/${userId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + checkUserObject(resource); + done(); + }); + }); + + describe("The POST /workspaces/users route (add)", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}/users`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 403 user is not workspace moderator", async done => { + const userId = testDbService.users[0].id; + const anotherUserId = testDbService.users[1].id; + const workspaceId = testDbService.workspaces[0].workspace.id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + user_id: anotherUserId, + role: "moderator", + }, + }, + }); + expect(response.statusCode).toBe(403); + done(); + }); + + it("should 400 when requested user not in company", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + user_id: nonExistentId, + role: "moderator", + }, + }, + }); + expect(response.statusCode).toBe(400); + done(); + }); + + it("should 201 when requested already in workspace (ignore)", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + user_id: userId, + role: "moderator", + }, + }, + }); + expect(response.statusCode).toBe(201); + done(); + }); + + it("should 200 when ok", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; + const anotherUserId = testDbService.workspaces[0].users[0].id; + + let workspaceUsersCount = await testDbService.getWorkspaceUsersCountFromDb(workspaceId); + let companyUsersCount = await testDbService.getCompanyUsersCountFromDb(companyId); + + console.log(testDbService.workspaces[2].users); + console.log(workspaceUsersCount); + + expect(workspaceUsersCount).toBe(4); + // expect(companyUsersCount).toBe(6); + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + user_id: anotherUserId, + role: "moderator", + }, + }, + }); + + expect(response.statusCode).toBe(201); + const resource = response.json()["resource"]; + checkUserObject(resource); + + workspaceUsersCount = await testDbService.getWorkspaceUsersCountFromDb(workspaceId); + companyUsersCount = await testDbService.getCompanyUsersCountFromDb(companyId); + expect(workspaceUsersCount).toBe(5); + // expect(companyUsersCount).toBe(6); + + done(); + }); + }); + + describe("The POST /workspaces/users/:user_id route (update)", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + const userId = testDbService.users[0].id; + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}/users/${userId}`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 403 user is not workspace moderator", async done => { + const userId = testDbService.users[0].id; + const anotherUserId = testDbService.users[1].id; + const workspaceId = testDbService.workspaces[0].workspace.id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users/${userId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + user_id: anotherUserId, + role: "moderator", + }, + }, + }); + expect(response.statusCode).toBe(403); + done(); + }); + + it("should 404 when user not found in workspace", async done => { + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; + const anotherWorkspaceUserId = testDbService.workspaces[3].users[0].id; + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users/${anotherWorkspaceUserId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + role: "moderator", + }, + }, + }); + + expect(response.statusCode).toBe(404); + done(); + }); + + it("should 200 when ok", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; + const anotherUserId = testDbService.workspaces[2].users[2].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users/${anotherUserId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + role: "moderator", + }, + }, + }); + + expect(response.statusCode).toBe(201); + const resource = response.json()["resource"]; + checkUserObject(resource); + + expect(resource["role"]).toBe("moderator"); + + const usersCount = await testDbService.getWorkspaceUsersCountFromDb(workspaceId); + expect(usersCount).toBe(5); + + done(); + }); + }); + + describe("The DELETE /workspaces/users/:user_id route", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + const anotherUserId = testDbService.users[1].id; + + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}/users/${anotherUserId}`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 403 user is not workspace moderator", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[3].id; + const anotherUserId = testDbService.workspaces[2].users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users/${anotherUserId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + + console.log(response.body); + + expect(response.statusCode).toBe(403); + done(); + }); + + it("should 404 when user not found in workspace", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users/${nonExistentId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + + expect(response.statusCode).toBe(404); + + done(); + }); + + it("should 200 when ok", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; + const anotherUserId = testDbService.workspaces[2].users[2].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + let response = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users/${anotherUserId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + + expect(response.statusCode).toBe(204); + + response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}/users`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + + expect(response.statusCode).toBe(200); + const resources = response.json()["resources"]; + expect(resources.find((a: { user_id: uuid }) => a.user_id === anotherUserId)).toBeUndefined(); + + const usersCount = await testDbService.getWorkspaceUsersCountFromDb(workspaceId); + expect(usersCount).toBe(4); + + done(); + }); + }); +}); diff --git a/twake/backend/node/test/e2e/workspaces/workspaces.spec.ts b/twake/backend/node/test/e2e/workspaces/workspaces.spec.ts new file mode 100644 index 00000000..d251e304 --- /dev/null +++ b/twake/backend/node/test/e2e/workspaces/workspaces.spec.ts @@ -0,0 +1,513 @@ +import { afterAll, beforeAll, describe, expect, it } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import { TestDbService } from "../utils.prepare.db"; +import { v1 as uuidv1 } from "uuid"; + +describe("The /workspaces API", () => { + const url = "/internal/services/workspaces/v1"; + let platform: TestPlatform; + + let testDbService: TestDbService; + + const nonExistentId = uuidv1(); + let companyId = ""; + + beforeAll(async ends => { + platform = await init({ + services: [ + "database", + "message-queue", + "webserver", + "user", + "search", + "workspaces", + "auth", + "console", + "counter", + "storage", + "applications", + "statistics", + "platform-services", + ], + }); + + companyId = platform.workspace.company_id; + + await platform.database.getConnector().init(); + testDbService = new TestDbService(platform); + await testDbService.createCompany(companyId); + const ws0pk = { id: uuidv1(), company_id: companyId }; + const ws1pk = { id: uuidv1(), company_id: companyId }; + const ws2pk = { id: uuidv1(), company_id: companyId }; + await testDbService.createWorkspace(ws0pk); + await testDbService.createWorkspace(ws1pk); + await testDbService.createWorkspace(ws2pk); + await testDbService.createUser([ws0pk, ws1pk]); + await testDbService.createUser([ws2pk], { companyRole: "admin" }); + await testDbService.createUser([ws2pk], { companyRole: undefined, workspaceRole: "moderator" }); + await testDbService.createUser([], { companyRole: "guest" }); + ends(); + }); + + afterAll(async ends => { + await platform.tearDown(); + ends(); + }); + + describe("The GET /workspaces/ route", () => { + it("should 401 when not authenticated", async done => { + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${nonExistentId}/workspaces`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 404 when company not found", async done => { + const userId = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${nonExistentId}/workspaces`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(404); + done(); + }); + + it("should 200 when company belongs to user", async done => { + const userId = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const companyId = testDbService.company.id; + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(200); + + const resources = response.json()["resources"]; + + expect(resources.length).toBe(2); + + for (const resource of resources) { + expect(resource).toMatchObject({ + id: expect.any(String), + company_id: expect.any(String), + name: expect.any(String), + logo: expect.any(String), + default: expect.any(Boolean), + archived: expect.any(Boolean), + role: expect.stringMatching(/moderator|member/), + }); + + if (resource.stats) { + expect(resource.stats).toMatchObject({ + created_at: expect.any(Number), + total_members: 1, + }); + } + } + + done(); + }); + }); + + describe("The GET /workspaces/:workspace_id route", () => { + it("should 401 when not authenticated", async done => { + const workspaceId = testDbService.workspaces[0].workspace.id; + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${nonExistentId}/workspaces/${workspaceId}`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 404 when workspace not found", async done => { + const userId = testDbService.workspaces[0].users[0].id; + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${uuidv1()}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(404); + done(); + }); + + it("should 403 when user not belong to workspace and not company_admin", async done => { + const workspaceId = testDbService.workspaces[2].workspace.id; + const userIdFromAnotherWorkspace = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userIdFromAnotherWorkspace }); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(403); + + expect(response.json()).toEqual({ + error: "Forbidden", + message: `You are not belong to workspace ${workspaceId}`, + statusCode: 403, + }); + + done(); + }); + + it("should 200 when user is company_admin", async done => { + const workspaceId = testDbService.workspaces[0].workspace.id; + const userIdFromAnotherWorkspace = testDbService.workspaces[2].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userIdFromAnotherWorkspace }); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + + expect(resource).toMatchObject({ + id: expect.any(String), + company_id: expect.any(String), + name: expect.any(String), + logo: expect.any(String), + default: expect.any(Boolean), + archived: expect.any(Boolean), + }); + + if (resource.stats) { + expect(resource.stats).toMatchObject({ + created_at: expect.any(Number), + total_members: 1, + }); + } + + done(); + }); + + it("should 200 when user is belong to workspace", async done => { + const workspaceId = testDbService.workspaces[0].workspace.id; + const userIdFromThisWorkspace = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userIdFromThisWorkspace }); + + const response = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + + expect(resource).toMatchObject({ + id: expect.any(String), + company_id: expect.any(String), + name: expect.any(String), + logo: expect.any(String), + default: expect.any(Boolean), + archived: expect.any(Boolean), + role: expect.stringMatching(/moderator|member/), + }); + + if (resource.stats) { + expect(resource.stats).toMatchObject({ + created_at: expect.any(Number), + total_members: 1, + }); + } + + done(); + }); + }); + + // create + + describe("The POST /workspaces route (creating workspace)", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 403 when user is not (company member or company admin) ", async done => { + const companyId = testDbService.company.id; + const userId = testDbService.users[3].id; + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + name: "Some channel name", + logo: "", + default: false, + }, + }, + }); + + expect(response.statusCode).toBe(403); + done(); + }); + + it("should 201 when workspace created well", async done => { + const companyId = testDbService.company.id; + const userId = testDbService.users[0].id; + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + name: "Random channel name", + logo: "logo", + default: false, + }, + }, + }); + + expect(response.statusCode).toBe(201); + + const resource = response.json()["resource"]; + + expect(resource).toMatchObject({ + id: expect.any(String), + company_id: expect.any(String), + name: expect.any(String), + logo: expect.any(String), + default: expect.any(Boolean), + archived: expect.any(Boolean), + role: expect.stringMatching(/moderator/), + }); + + done(); + }); + }); + + // update + + describe("The POST /workspaces/:workspace_id route (updating workspace)", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}`, + payload: { resource: {} }, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 403 when not workspace not found", async done => { + const companyId = testDbService.company.id; + const userId = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { resource: {} }, + }); + + expect(response.statusCode).toBe(403); + + done(); + }); + + it("should 403 when not belong to workspace", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[1].workspace.id; + const userId = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { resource: {} }, + }); + + expect(response.statusCode).toBe(403); + + done(); + }); + + it("should 403 when not workspace moderator", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[1].workspace.id; + const userId = testDbService.workspaces[0].users[0].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { resource: {} }, + }); + + expect(response.statusCode).toBe(403); + + done(); + }); + + it("should 200 when admin of company (full update)", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[0].id; // company owner + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + name: "Another workspace name", + logo: "logo", + default: false, + archived: false, + }, + }, + }); + + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + + expect(resource).toMatchObject({ + id: workspaceId, + company_id: companyId, + name: "Another workspace name", + logo: expect.any(String), + default: false, + archived: false, + role: "moderator", //Company admin is a moderator + }); + + done(); + }); + + it("should 200 when moderator of workspace (partial update)", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[1].id; // workspace admin + + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "POST", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + payload: { + resource: { + name: "My awesome workspace", + default: true, + logo: "", + }, + }, + }); + + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + + expect(resource).toMatchObject({ + id: workspaceId, + company_id: companyId, + name: "My awesome workspace", + logo: expect.any(String), + default: true, + archived: false, + role: "moderator", + }); + + done(); + }); + }); + + // delete + + describe("The DELETE /workspaces route", () => { + it("should 401 when not authenticated", async done => { + const companyId = testDbService.company.id; + + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${companyId}/workspaces/${nonExistentId}`, + }); + expect(response.statusCode).toBe(401); + done(); + }); + + it("should 403 when user is not (company member or company admin) ", async done => { + const companyId = testDbService.company.id; + const userId = testDbService.users[3].id; + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + const workspaceId = testDbService.workspaces[0].workspace.id; + + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + + expect(response.statusCode).toBe(403); + done(); + }); + + it("should 204 when workspace deleted", async done => { + const companyId = testDbService.company.id; + const workspaceId = testDbService.workspaces[2].workspace.id; + const userId = testDbService.workspaces[2].users[0].id; + const jwtToken = await platform.auth.getJWTToken({ sub: userId }); + + const response = await platform.app.inject({ + method: "DELETE", + url: `${url}/companies/${companyId}/workspaces/${workspaceId}`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + + expect(response.statusCode).toBe(204); + + const checkResponse = await platform.app.inject({ + method: "GET", + url: `${url}/companies/${companyId}/workspaces`, + headers: { authorization: `Bearer ${jwtToken}` }, + }); + + const checkResponseJson = checkResponse.json(); + + expect(checkResponseJson.resources.find((a: any) => a.id === workspaceId)).toBe(undefined); + + done(); + }); + }); +}); From d037cdc6fd5890c6b8b1c09bfcda65b52e2c6213 Mon Sep 17 00:00:00 2001 From: montaghanmy Date: Mon, 10 Apr 2023 13:10:07 +0100 Subject: [PATCH 8/8] ref: applications/channels --- .../applications/services/applications.ts | 3 - .../services/internal-event-to-hooks.ts | 64 ------------------- .../channels/services/member/service.ts | 3 +- .../node/src/services/channels/types.ts | 4 -- .../node/src/services/global-resolver.ts | 13 ++++ 5 files changed, 14 insertions(+), 73 deletions(-) delete mode 100644 twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts diff --git a/twake/backend/node/src/services/applications/services/applications.ts b/twake/backend/node/src/services/applications/services/applications.ts index 4eecdd8f..b2891be3 100644 --- a/twake/backend/node/src/services/applications/services/applications.ts +++ b/twake/backend/node/src/services/applications/services/applications.ts @@ -18,7 +18,6 @@ import SearchRepository from "../../../core/platform/services/search/repository" import assert from "assert"; import gr from "../../global-resolver"; -import { InternalToHooksProcessor } from "./internal-event-to-hooks"; export class ApplicationServiceImpl implements TwakeServiceProvider, Initializable { version: "1"; @@ -37,8 +36,6 @@ export class ApplicationServiceImpl implements TwakeServiceProvider, Initializab logger.error("Error while initializing applications service"); } - gr.platformServices.messageQueue.processor.addHandler(new InternalToHooksProcessor()); - return this; } diff --git a/twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts b/twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts deleted file mode 100644 index 77fe8d74..00000000 --- a/twake/backend/node/src/services/applications/services/internal-event-to-hooks.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { logger } from "../../../core/platform/framework"; -import { HookType } from "../../applications-api/types"; -import { MessageQueueHandler } from "../../../core/platform/services/message-queue/api"; -import { MessageHook } from "../../messages/types"; -import gr from "../../global-resolver"; -import { ExecutionContext } from "../../../core/platform/framework/api/crud-service"; - -export class InternalToHooksProcessor implements MessageQueueHandler { - readonly topics = { - in: "application:hook:message", - }; - - readonly options = { - unique: true, - ack: true, - queue: "application:hook:message:consumer1", - }; - - readonly name = "Application::InternalToHooksProcessor"; - - validate(_: HookType): boolean { - return true; - } - - async process(message: HookType, context?: ExecutionContext): Promise { - logger.debug(`${this.name} - Receive hook of type ${message.type}`); - - const application = await gr.services.applications.marketplaceApps.get( - { - id: message.application_id, - }, - context, - ); - - //TODO Check application access rights (hooks) - const _access = application.access; - - // Check application still exists in the company - if ( - !(await gr.services.applications.companyApps.get({ - company_id: message.company_id, - application_id: message.application_id, - id: undefined, - })) - ) { - logger.error( - `${this.name} - Application ${message.application_id} not found in company ${message.company_id}`, - ); - return; - } - - await gr.services.applications.hooks.notifyApp( - message.application_id, - null, - null, - "hook", - null, - { message }, - null, - null, - context, - ); - } -} diff --git a/twake/backend/node/src/services/channels/services/member/service.ts b/twake/backend/node/src/services/channels/services/member/service.ts index 014f19ce..f16aadbd 100644 --- a/twake/backend/node/src/services/channels/services/member/service.ts +++ b/twake/backend/node/src/services/channels/services/member/service.ts @@ -26,7 +26,6 @@ import { ChannelMemberType, ChannelVisibility, WorkspaceExecutionContext, - CompanyExecutionContext, } from "../../types"; import { Channel, ResourceEventsPayload, User } from "../../../../utils/types"; import { cloneDeep, isNil, omitBy } from "lodash"; @@ -53,7 +52,7 @@ import { WorkspacePrimaryKey } from "../../../workspaces/entities/workspace"; import gr from "../../../global-resolver"; import uuidTime from "uuid-time"; import { ChannelObject } from "../channel/types"; - +import { CompanyExecutionContext } from "../../../applications/web/types"; const USER_CHANNEL_KEYS = [ "id", "company_id", diff --git a/twake/backend/node/src/services/channels/types.ts b/twake/backend/node/src/services/channels/types.ts index 0b9e4a28..20894579 100644 --- a/twake/backend/node/src/services/channels/types.ts +++ b/twake/backend/node/src/services/channels/types.ts @@ -52,7 +52,3 @@ export type ChannelActivityNotification = { sender_name: string; body: string; }; - -export interface CompanyExecutionContext extends ExecutionContext { - company: { id: string }; -} diff --git a/twake/backend/node/src/services/global-resolver.ts b/twake/backend/node/src/services/global-resolver.ts index a7f111ed..dd04c0af 100644 --- a/twake/backend/node/src/services/global-resolver.ts +++ b/twake/backend/node/src/services/global-resolver.ts @@ -25,6 +25,8 @@ import { UserExternalLinksServiceImpl } from "./user/services/external_links"; import { UserNotificationBadgeService } from "./notifications/services/bages"; import { NotificationPreferencesService } from "./notifications/services/preferences"; import { UserServiceImpl } from "./user/services/users/service"; +import { CompanyApplicationServiceImpl } from "./applications/services/company-applications"; +import { ApplicationServiceImpl } from "./applications/services/applications"; import { FileServiceImpl } from "./files/services"; import { ChannelServiceImpl } from "./channels/services/channel/service"; import { MemberServiceImpl } from "./channels/services/member/service"; @@ -36,6 +38,7 @@ import { NotificationEngine } from "./notifications/services/engine"; import { MobilePushService } from "./notifications/services/mobile-push"; import { ChannelMemberPreferencesServiceImpl } from "./notifications/services/channel-preferences"; import { ChannelThreadUsersServiceImpl } from "./notifications/services/channel-thread-users"; +import { ApplicationHooksService } from "./applications/services/hooks"; import OnlineServiceImpl from "./online/service"; import { ChannelsMessageQueueListener } from "./channels/services/pubsub"; import { UserNotificationDigestService } from "./notifications/services/digest"; @@ -75,6 +78,11 @@ type TwakeServices = { mobilePush: MobilePushService; digest: UserNotificationDigestService; }; + applications: { + marketplaceApps: ApplicationServiceImpl; + companyApps: CompanyApplicationServiceImpl; + hooks: ApplicationHooksService; + }; files: FileServiceImpl; channels: { channels: ChannelServiceImpl; @@ -145,6 +153,11 @@ class GlobalResolver { mobilePush: await new MobilePushService().init(), digest: await new UserNotificationDigestService().init(), }, + applications: { + marketplaceApps: await new ApplicationServiceImpl().init(), + companyApps: await new CompanyApplicationServiceImpl().init(), + hooks: await new ApplicationHooksService().init(), + }, files: await new FileServiceImpl().init(), channels: { channels: await new ChannelServiceImpl().init(),