From 1827e950fb5e50539f1b7a04f937016828db3188 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Sat, 22 Jun 2024 17:56:07 -0300 Subject: [PATCH 001/114] chore: report flaky tests to jira (#32593) --- .github/workflows/ci-test-e2e.yml | 7 ++ .github/workflows/ci.yml | 3 + apps/meteor/playwright.config.ts | 15 +++ apps/meteor/reporters/jira.ts | 186 ++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 apps/meteor/reporters/jira.ts diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index b46c124d149be..920aea0aa3087 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -67,6 +67,8 @@ on: required: false CODECOV_TOKEN: required: false + REPORTER_JIRA_ROCKETCHAT_API_KEY: + required: false env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true @@ -250,10 +252,15 @@ jobs: IS_EE: ${{ inputs.release == 'ee' && 'true' || '' }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && 'true' || '' }} REPORTER_ROCKETCHAT_RUN: ${{ github.run_number }} REPORTER_ROCKETCHAT_BRANCH: ${{ github.ref }} REPORTER_ROCKETCHAT_DRAFT: ${{ github.event.pull_request.draft }} + REPORTER_ROCKETCHAT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPORTER_ROCKETCHAT_AUTHOR: ${{ github.event.pull_request.user.login }} + REPORTER_ROCKETCHAT_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + REPORTER_ROCKETCHAT_PR: ${{ github.event.pull_request.number }} QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} QASE_REPORT: ${{ github.ref == 'refs/heads/develop' && 'true' || '' }} CI: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9aa8f6168579..ce18d17b8b80a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -349,6 +349,7 @@ jobs: QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} test-api-ee: name: 🔨 Test API (EE) @@ -400,6 +401,7 @@ jobs: REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} test-ui-ee-no-watcher: name: 🔨 Test UI (EE) @@ -430,6 +432,7 @@ jobs: REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} tests-done: name: ✅ Tests Done diff --git a/apps/meteor/playwright.config.ts b/apps/meteor/playwright.config.ts index d40592b8f71f9..822f78e28741f 100644 --- a/apps/meteor/playwright.config.ts +++ b/apps/meteor/playwright.config.ts @@ -30,6 +30,21 @@ export default { branch: process.env.REPORTER_ROCKETCHAT_BRANCH, run: Number(process.env.REPORTER_ROCKETCHAT_RUN), draft: process.env.REPORTER_ROCKETCHAT_DRAFT === 'true', + headSha: process.env.REPORTER_ROCKETCHAT_HEAD_SHA, + }, + ], + process.env.REPORTER_ROCKETCHAT_REPORT === 'true' && [ + './reporters/jira.ts', + { + url: `https://rocketchat.atlassian.net`, + apiKey: process.env.REPORTER_JIRA_ROCKETCHAT_API_KEY ?? process.env.JIRA_TOKEN, + branch: process.env.REPORTER_ROCKETCHAT_BRANCH, + run: Number(process.env.REPORTER_ROCKETCHAT_RUN), + headSha: process.env.REPORTER_ROCKETCHAT_HEAD_SHA, + author: process.env.REPORTER_ROCKETCHAT_AUTHOR, + run_url: process.env.REPORTER_ROCKETCHAT_RUN_URL, + pr: Number(process.env.REPORTER_ROCKETCHAT_PR), + draft: process.env.REPORTER_ROCKETCHAT_DRAFT === 'true', }, ], [ diff --git a/apps/meteor/reporters/jira.ts b/apps/meteor/reporters/jira.ts new file mode 100644 index 0000000000000..706856389003a --- /dev/null +++ b/apps/meteor/reporters/jira.ts @@ -0,0 +1,186 @@ +import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter'; +import fetch from 'node-fetch'; + +class JIRAReporter implements Reporter { + private url: string; + + private apiKey: string; + + private branch: string; + + private draft: boolean; + + private run: number; + + private headSha: string; + + private author: string; + + private run_url: string; + + private pr: number; + + constructor(options: { + url: string; + apiKey: string; + branch: string; + draft: boolean; + run: number; + headSha: string; + author: string; + run_url: string; + pr: number; + }) { + this.url = options.url; + this.apiKey = options.apiKey; + this.branch = options.branch; + this.draft = options.draft; + this.run = options.run; + this.headSha = options.headSha; + this.author = options.author; + this.run_url = options.run_url; + this.pr = options.pr; + } + + async onTestEnd(test: TestCase, result: TestResult) { + if (process.env.REPORTER_ROCKETCHAT_REPORT !== 'true') { + return; + } + + if (this.draft === true) { + return; + } + + if (result.status === 'passed' || result.status === 'skipped') { + return; + } + + const payload = { + name: test.title, + status: result.status, + duration: result.duration, + branch: this.branch, + draft: this.draft, + run: this.run, + headSha: this.headSha, + }; + + console.log(`Sending test result to JIRA: ${JSON.stringify(payload)}`); + + // first search and check if there is an existing issue + + const search = await fetch( + `${this.url}/rest/api/2/search?${new URLSearchParams({ + jql: `project = FLAKY AND summary ~ '${payload.name}'`, + })}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }, + ); + + if (!search.ok) { + throw new Error( + `JIRA: Failed to search for existing issue: ${search.statusText}.` + + `${this.url}/rest/api/2/search${new URLSearchParams({ + jql: `project = FLAKY AND summary ~ '${payload.name}'`, + })}`, + ); + } + + const { issues } = await search.json(); + + const existing = issues.find( + (issue: { + fields: { + summary: string; + }; + }) => issue.fields.summary === payload.name, + ); + + if (existing) { + const { location } = test; + + await fetch(`${this.url}/rest/api/2/issue/${existing.key}/comment`, { + method: 'POST', + body: JSON.stringify({ + body: `Test run ${payload.run} failed +author: ${this.author} +PR: ${this.pr} +https://github.com/RocketChat/Rocket.Chat/blob/${payload.headSha}/${location.file.replace( + '/home/runner/work/Rocket.Chat/Rocket.Chat', + '', + )}#L${location.line}:${location.column} +${this.run_url} +`, + }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }); + return; + } + + const data: { + fields: { + summary: string; + description: string; + issuetype: { + name: string; + }; + project: { + key: string; + }; + }; + } = { + fields: { + summary: payload.name, + description: '', + issuetype: { + name: 'Tech Debt', + }, + project: { + key: 'FLAKY', + }, + }, + }; + + const responseIssue = await fetch(`${this.url}/rest/api/2/issue`, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }); + + const issue = (await responseIssue.json()).key; + + const { location } = test; + + await fetch(`${this.url}/rest/api/2/issue/${issue}/comment`, { + method: 'POST', + body: JSON.stringify({ + body: `Test run ${payload.run} failed +author: ${this.author} +PR: ${this.pr} +https://github.com/RocketChat/Rocket.Chat/blob/${payload.headSha}/${location.file.replace( + '/home/runner/work/Rocket.Chat/Rocket.Chat', + '', + )}#L${location.line}:${location.column}, +${this.run_url} +`, + }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${this.apiKey}`, + }, + }); + } +} + +export default JIRAReporter; From c02d45155e3780a7436592a69e81cf0b6b85d054 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 24 Jun 2024 16:17:52 -0300 Subject: [PATCH 002/114] ci: Use `actions/checkout` (#32635) Co-authored-by: Guilherme Gazzo --- .github/workflows/ci.yml | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce18d17b8b80a..0c4969dd8e76b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,12 @@ jobs: # to avoid this, we are using a dummy license, expiring at 2024-06-30 enterprise-license: WMa5i+/t/LZbYOj8u3XUkivRhWBtWO6ycUjaZoVAw2DxMfdyBIAa2gMMI4x7Z2BrTZIZhFEImfOxcXcgD0QbXHGBJaMI+eYG+eofnVWi2VA7RWbpvWTULgPFgyJ4UEFeCOzVjcBLTQbmMSam3u0RlekWJkfAO0KnmLtsaEYNNA2rz1U+CLI/CdNGfdqrBu5PZZbGkH0KEzyIZMaykOjzvX+C6vd7fRxh23HecwhkBbqE8eQsCBt2ad0qC4MoVXsDaSOmSzGW+aXjuXt/9zjvrLlsmWQTSlkrEHdNkdywm0UkGxqz3+CP99n0WggUBioUiChjMuNMoceWvDvmxYP9Ml2NpYU7SnfhjmMFyXOah8ofzv8w509Y7XODvQBz+iB4Co9YnF3vT96HDDQyAV5t4jATE+0t37EAXmwjTi3qqyP7DLGK/revl+mlcwJ5kS4zZBsm1E4519FkXQOZSyWRnPdjqvh4mCLqoispZ49wKvklDvjPxCSP9us6cVXLDg7NTJr/4pfxLPOkvv7qCgugDvlDx17bXpQFPSDxmpw66FLzvb5Id0dkWjOzrRYSXb0bFWoUQjtHFzmcpFkyVhOKrQ9zA9+Zm7vXmU9Y2l2dK79EloOuHMSYAqsPEag8GMW6vI/cT4iIjHGGDePKnD0HblvTEKzql11cfT/abf2IiaY= steps: - - uses: Bhacaz/checkout-files@v2 + - uses: actions/checkout@v4 with: - files: package.json - branch: ${{ github.ref }} + sparse-checkout: | + package.json + sparse-checkout-cone-mode: false + ref: ${{ github.ref }} - id: var run: | @@ -85,10 +87,12 @@ jobs: runs-on: ubuntu-20.04 needs: [release-versions] steps: - - uses: Bhacaz/checkout-files@v2 + - uses: actions/checkout@v4 with: - files: package.json - branch: ${{ github.ref }} + sparse-checkout: | + package.json + sparse-checkout-cone-mode: false + ref: ${{ github.ref }} - name: Register release on cloud as Draft if: github.event_name == 'release' @@ -451,10 +455,12 @@ jobs: needs: [build-gh-docker, release-versions] steps: - - uses: Bhacaz/checkout-files@v2 + - uses: actions/checkout@v4 with: - files: package.json - branch: ${{ github.ref }} + sparse-checkout: | + package.json + sparse-checkout-cone-mode: false + ref: ${{ github.ref }} - name: Restore build uses: actions/download-artifact@v3 @@ -734,10 +740,12 @@ jobs: - docker-image-publish - release-versions steps: - - uses: Bhacaz/checkout-files@v2 + - uses: actions/checkout@v4 with: - files: package.json - branch: ${{ github.ref }} + sparse-checkout: | + package.json + sparse-checkout-cone-mode: false + ref: ${{ github.ref }} - name: Releases service env: From 6f935c8ba11e7146dcb5af70de7d960a5127d2e8 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 24 Jun 2024 18:34:25 -0300 Subject: [PATCH 003/114] refactor: Widen types for message rendering helpers (#32601) --- apps/meteor/app/emoji/client/emojiParser.js | 18 ++++++------------ apps/meteor/app/markdown/lib/markdown.js | 2 ++ .../markdown/lib/parser/filtered/filtered.js | 5 +++-- .../client/lib/utils/renderMessageEmoji.ts | 6 ++---- .../Threads/components/ThreadTitle.tsx | 2 +- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/apps/meteor/app/emoji/client/emojiParser.js b/apps/meteor/app/emoji/client/emojiParser.js index 7b887bb0575fe..0b3b722aaebdf 100644 --- a/apps/meteor/app/emoji/client/emojiParser.js +++ b/apps/meteor/app/emoji/client/emojiParser.js @@ -1,17 +1,13 @@ import { isIE11 } from '../../../client/lib/utils/isIE11'; import { emoji } from './lib'; -/* +/** * emojiParser is a function that will replace emojis - * @param {Object} message - The message object + * @param {{ html: string }} message - The message object + * @return {{ html: string }} */ - -const emojiParser = (message) => { - if (!message.html?.trim()) { - return message; - } - - let html = message.html.trim(); +export const emojiParser = ({ html }) => { + html = html.trim(); // ' to apostrophe (') for emojis such as :') html = html.replace(/'/g, "'"); @@ -64,7 +60,5 @@ const emojiParser = (message) => { // line breaks '
' back to '
' html = html.replace(/
/g, '
'); - return { ...message, html }; + return { html }; }; - -export { emojiParser }; diff --git a/apps/meteor/app/markdown/lib/markdown.js b/apps/meteor/app/markdown/lib/markdown.js index 3c3acdb178938..c7fe452e08291 100644 --- a/apps/meteor/app/markdown/lib/markdown.js +++ b/apps/meteor/app/markdown/lib/markdown.js @@ -69,6 +69,7 @@ class MarkdownClass { return code(...args); } + /** @param {string} message */ filterMarkdownFromMessage(message) { return parsers.filtered(message); } @@ -76,6 +77,7 @@ class MarkdownClass { export const Markdown = new MarkdownClass(); +/** @param {string} message */ export const filterMarkdown = (message) => Markdown.filterMarkdownFromMessage(message); export const createMarkdownMessageRenderer = ({ ...options }) => { diff --git a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js b/apps/meteor/app/markdown/lib/parser/filtered/filtered.js index ac53144d6d1b1..260fc835d8a0a 100644 --- a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js +++ b/apps/meteor/app/markdown/lib/parser/filtered/filtered.js @@ -1,6 +1,7 @@ -/* +/** * Filter markdown tags in message - * Use case: notifications + * Use case: notifications + * @param {string} message */ export const filtered = ( message, diff --git a/apps/meteor/client/lib/utils/renderMessageEmoji.ts b/apps/meteor/client/lib/utils/renderMessageEmoji.ts index 20986c803ab8b..7960ec1914e59 100644 --- a/apps/meteor/client/lib/utils/renderMessageEmoji.ts +++ b/apps/meteor/client/lib/utils/renderMessageEmoji.ts @@ -1,5 +1,3 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import { emojiParser } from '../../../app/emoji/client/emojiParser'; -import { emojiParser } from '../../../app/emoji/client/emojiParser.js'; - -export const renderMessageEmoji = & { html?: string }>(message: T): string => emojiParser(message)?.html; +export const renderMessageEmoji = ({ html }: { html: string }): string => emojiParser({ html }).html; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx index 6440a5e726a1b..ada559d7af7d5 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx @@ -9,7 +9,7 @@ type ThreadTitleProps = { }; const ThreadTitle = ({ mainMessage }: ThreadTitleProps) => { - const innerHTML = useMemo(() => ({ __html: normalizeThreadTitle(mainMessage) }), [mainMessage]); + const innerHTML = useMemo(() => ({ __html: normalizeThreadTitle(mainMessage) ?? '' }), [mainMessage]); return ; }; From e07a5158d69e7472f45b210b0920249c03f5a817 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 24 Jun 2024 18:56:17 -0300 Subject: [PATCH 004/114] refactor(tests): API tests (#32643) --- apps/meteor/.eslintignore | 1 - apps/meteor/.mocharc.api.js | 9 +- apps/meteor/package.json | 2 +- apps/meteor/tests/data/api-data.js | 68 -- apps/meteor/tests/data/api-data.ts | 76 ++ apps/meteor/tests/data/apps/apps-data.js | 4 - apps/meteor/tests/data/apps/apps-data.ts | 12 + .../tests/data/apps/{helper.js => helper.ts} | 20 +- apps/meteor/tests/data/channel.ts | 1 - .../data/{chat.helper.js => chat.helper.ts} | 41 +- apps/meteor/tests/data/checks.js | 15 - apps/meteor/tests/data/constants.ts | 2 - apps/meteor/tests/data/custom-fields.js | 18 - ...ration.helper.js => integration.helper.ts} | 16 +- apps/meteor/tests/data/interactions.ts | 5 - apps/meteor/tests/data/licenses.helper.ts | 5 - .../tests/data/livechat/businessHours.ts | 210 ++--- .../tests/data/livechat/canned-responses.ts | 25 +- .../tests/data/livechat/custom-fields.ts | 111 +-- apps/meteor/tests/data/livechat/department.ts | 182 ++-- apps/meteor/tests/data/livechat/inboxes.ts | 56 +- apps/meteor/tests/data/livechat/inquiries.ts | 9 +- apps/meteor/tests/data/livechat/priorities.ts | 13 +- apps/meteor/tests/data/livechat/rooms.ts | 120 +-- apps/meteor/tests/data/livechat/tags.ts | 14 +- apps/meteor/tests/data/livechat/triggers.ts | 47 +- apps/meteor/tests/data/livechat/units.ts | 63 +- apps/meteor/tests/data/livechat/users.ts | 87 +- apps/meteor/tests/data/livechat/utils.ts | 7 +- apps/meteor/tests/data/livechat/visitor.ts | 14 +- apps/meteor/tests/data/moderation.helper.ts | 17 - apps/meteor/tests/data/permissions.helper.ts | 108 +-- apps/meteor/tests/data/role.ts | 5 - apps/meteor/tests/data/rooms.helper.ts | 87 +- apps/meteor/tests/data/subscriptions.ts | 15 - apps/meteor/tests/data/teams.helper.ts | 32 +- apps/meteor/tests/data/uploads.helper.ts | 60 +- apps/meteor/tests/data/user.ts | 41 +- apps/meteor/tests/data/users.helper.js | 121 --- apps/meteor/tests/data/users.helper.ts | 102 +++ apps/meteor/tests/e2e/config/constants.ts | 2 +- ...0-autotranslate.js => 00-autotranslate.ts} | 116 +-- ...0-miscellaneous.js => 00-miscellaneous.ts} | 106 +-- .../api/{01-users.js => 01-users.ts} | 784 +++++++++++------- .../api/{02-channels.js => 02-channels.ts} | 313 +++---- .../api/{03-groups.js => 03-groups.ts} | 222 ++--- ...direct-message.js => 04-direct-message.ts} | 146 ++-- .../end-to-end/api/{05-chat.js => 05-chat.ts} | 486 ++++++----- ...rations.js => 06-outgoing-integrations.ts} | 138 +-- ...rations.js => 07-incoming-integrations.ts} | 172 ++-- .../api/{08-settings.js => 08-settings.ts} | 29 +- .../api/{09-rooms.js => 09-rooms.ts} | 476 +++++------ ...0-subscriptions.js => 10-subscriptions.ts} | 61 +- .../{11-permissions.js => 11-permissions.ts} | 26 +- ...{12-emoji-custom.js => 12-emoji-custom.ts} | 96 ++- .../api/{14-assets.js => 14-assets.ts} | 14 +- .../api/{16-commands.js => 16-commands.ts} | 47 +- ...7-custom-sounds.js => 17-custom-sounds.ts} | 24 +- ...ser-status.js => 17-custom-user-status.ts} | 10 +- .../api/{17-webdav.js => 17-webdav.ts} | 12 +- .../api/{18-oauthapps.js => 18-oauthapps.ts} | 52 +- .../{19-statistics.js => 19-statistics.ts} | 33 +- .../api/{20-licenses.js => 20-licenses.ts} | 36 +- .../api/{21-banners.js => 21-banners.ts} | 24 +- apps/meteor/tests/end-to-end/api/22-push.ts | 6 +- .../api/{23-invites.js => 23-invites.ts} | 40 +- .../api/{24-methods.js => 24-methods.ts} | 361 ++++---- .../api/{25-teams.js => 25-teams.ts} | 490 +++++------ apps/meteor/tests/end-to-end/api/26-LDAP.ts | 6 +- .../tests/end-to-end/api/27-moderation.ts | 25 +- .../tests/end-to-end/api/27-presence.ts | 5 +- apps/meteor/tests/end-to-end/api/28-roles.ts | 6 +- .../tests/end-to-end/api/29-oauth-server.ts | 6 +- .../tests/end-to-end/api/30-calendar.ts | 12 +- .../api/31-failed-login-attempts.ts | 16 +- apps/meteor/tests/end-to-end/api/32-assets.ts | 6 +- .../tests/end-to-end/api/33-federation.ts | 4 +- .../tests/end-to-end/api/import.spec.ts | 8 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 72 +- .../end-to-end/api/livechat/01-agents.ts | 29 +- .../end-to-end/api/livechat/02-appearance.ts | 4 +- .../api/livechat/03-custom-fields.ts | 4 +- .../end-to-end/api/livechat/04-dashboards.ts | 105 ++- .../end-to-end/api/livechat/05-inquiries.ts | 21 +- .../api/livechat/06-integrations.ts | 4 +- .../tests/end-to-end/api/livechat/07-queue.ts | 4 +- .../end-to-end/api/livechat/08-triggers.ts | 4 +- .../end-to-end/api/livechat/09-visitors.ts | 67 +- .../end-to-end/api/livechat/11-email-inbox.ts | 2 +- .../end-to-end/api/livechat/11-livechat.ts | 24 +- .../end-to-end/api/livechat/12-priorites.ts | 8 +- .../tests/end-to-end/api/livechat/13-tags.ts | 6 +- .../tests/end-to-end/api/livechat/14-units.ts | 4 +- .../api/livechat/15-canned-responses.ts | 4 +- .../end-to-end/api/livechat/16-video-call.ts | 4 +- .../api/livechat/17-dashboards-ee.ts | 4 +- .../end-to-end/api/livechat/18-rooms-ee.ts | 9 +- .../api/livechat/19-business-hours.ts | 49 +- .../end-to-end/api/livechat/21-reports.ts | 9 +- .../end-to-end/api/livechat/22-monitors.ts | 7 +- .../tests/end-to-end/api/livechat/23-mac.ts | 3 +- .../end-to-end/api/livechat/24-routing.ts | 16 +- .../livechat/methods/changeLivechatStatus.ts | 19 +- ...{00-installation.js => 00-installation.ts} | 34 +- ...1-send-messages.js => 01-send-messages.ts} | 38 +- ...as-user.js => 02-send-messages-as-user.ts} | 56 +- ...ple.js => 03-slash-command-test-simple.ts} | 21 +- ...> 04-slash-command-test-with-arguments.ts} | 15 +- .../end-to-end/apps/05-video-conferences.ts | 8 +- .../{apps-uninstall.js => apps-uninstall.ts} | 16 +- .../end-to-end/{teardown.js => teardown.ts} | 13 +- .../tests/unit/app/markdown/client.tests.js | 2 +- ee/packages/api-client/src/Credentials.ts | 4 + ee/packages/api-client/src/index.ts | 25 +- packages/core-typings/src/IRoom.ts | 2 - packages/core-typings/src/IUser.ts | 3 +- packages/password-policies/package.json | 2 +- packages/rest-typings/src/v1/rooms.ts | 2 +- .../src/ResetPassword/ResetPasswordPage.tsx | 3 +- yarn.lock | 18 +- 120 files changed, 3633 insertions(+), 3368 deletions(-) delete mode 100644 apps/meteor/tests/data/api-data.js create mode 100644 apps/meteor/tests/data/api-data.ts delete mode 100644 apps/meteor/tests/data/apps/apps-data.js create mode 100644 apps/meteor/tests/data/apps/apps-data.ts rename apps/meteor/tests/data/apps/{helper.js => helper.ts} (68%) delete mode 100644 apps/meteor/tests/data/channel.ts rename apps/meteor/tests/data/{chat.helper.js => chat.helper.ts} (54%) delete mode 100644 apps/meteor/tests/data/checks.js delete mode 100644 apps/meteor/tests/data/custom-fields.js rename apps/meteor/tests/data/{integration.helper.js => integration.helper.ts} (54%) delete mode 100644 apps/meteor/tests/data/licenses.helper.ts delete mode 100644 apps/meteor/tests/data/moderation.helper.ts delete mode 100644 apps/meteor/tests/data/role.ts delete mode 100644 apps/meteor/tests/data/subscriptions.ts delete mode 100644 apps/meteor/tests/data/users.helper.js create mode 100644 apps/meteor/tests/data/users.helper.ts rename apps/meteor/tests/end-to-end/api/{00-autotranslate.js => 00-autotranslate.ts} (88%) rename apps/meteor/tests/end-to-end/api/{00-miscellaneous.js => 00-miscellaneous.ts} (92%) rename apps/meteor/tests/end-to-end/api/{01-users.js => 01-users.ts} (88%) rename apps/meteor/tests/end-to-end/api/{02-channels.js => 02-channels.ts} (92%) rename apps/meteor/tests/end-to-end/api/{03-groups.js => 03-groups.ts} (94%) rename apps/meteor/tests/end-to-end/api/{04-direct-message.js => 04-direct-message.ts} (91%) rename apps/meteor/tests/end-to-end/api/{05-chat.js => 05-chat.ts} (92%) rename apps/meteor/tests/end-to-end/api/{06-outgoing-integrations.js => 06-outgoing-integrations.ts} (82%) rename apps/meteor/tests/end-to-end/api/{07-incoming-integrations.js => 07-incoming-integrations.ts} (81%) rename apps/meteor/tests/end-to-end/api/{08-settings.js => 08-settings.ts} (89%) rename apps/meteor/tests/end-to-end/api/{09-rooms.js => 09-rooms.ts} (91%) rename apps/meteor/tests/end-to-end/api/{10-subscriptions.js => 10-subscriptions.ts} (92%) rename apps/meteor/tests/end-to-end/api/{11-permissions.js => 11-permissions.ts} (91%) rename apps/meteor/tests/end-to-end/api/{12-emoji-custom.js => 12-emoji-custom.ts} (87%) rename apps/meteor/tests/end-to-end/api/{14-assets.js => 14-assets.ts} (94%) rename apps/meteor/tests/end-to-end/api/{16-commands.js => 16-commands.ts} (94%) rename apps/meteor/tests/end-to-end/api/{17-custom-sounds.js => 17-custom-sounds.ts} (94%) rename apps/meteor/tests/end-to-end/api/{17-custom-user-status.js => 17-custom-user-status.ts} (91%) rename apps/meteor/tests/end-to-end/api/{17-webdav.js => 17-webdav.ts} (92%) rename apps/meteor/tests/end-to-end/api/{18-oauthapps.js => 18-oauthapps.ts} (88%) rename apps/meteor/tests/end-to-end/api/{19-statistics.js => 19-statistics.ts} (84%) rename apps/meteor/tests/end-to-end/api/{20-licenses.js => 20-licenses.ts} (91%) rename apps/meteor/tests/end-to-end/api/{21-banners.js => 21-banners.ts} (94%) rename apps/meteor/tests/end-to-end/api/{23-invites.js => 23-invites.ts} (93%) rename apps/meteor/tests/end-to-end/api/{24-methods.js => 24-methods.ts} (95%) rename apps/meteor/tests/end-to-end/api/{25-teams.js => 25-teams.ts} (85%) rename apps/meteor/tests/end-to-end/apps/{00-installation.js => 00-installation.ts} (82%) rename apps/meteor/tests/end-to-end/apps/{01-send-messages.js => 01-send-messages.ts} (82%) rename apps/meteor/tests/end-to-end/apps/{02-send-messages-as-user.js => 02-send-messages-as-user.ts} (78%) rename apps/meteor/tests/end-to-end/apps/{03-slash-command-test-simple.js => 03-slash-command-test-simple.ts} (84%) rename apps/meteor/tests/end-to-end/apps/{04-slash-command-test-with-arguments.js => 04-slash-command-test-with-arguments.ts} (81%) rename apps/meteor/tests/end-to-end/apps/{apps-uninstall.js => apps-uninstall.ts} (85%) rename apps/meteor/tests/end-to-end/{teardown.js => teardown.ts} (59%) create mode 100644 ee/packages/api-client/src/Credentials.ts diff --git a/apps/meteor/.eslintignore b/apps/meteor/.eslintignore index 2bbdbae00b89a..2701a871d9814 100644 --- a/apps/meteor/.eslintignore +++ b/apps/meteor/.eslintignore @@ -1,6 +1,5 @@ /node_modules/ #/tests/e2e/ -/tests/data/ /packages/ /app/emoji-emojione/generateEmojiIndex.js /public/ diff --git a/apps/meteor/.mocharc.api.js b/apps/meteor/.mocharc.api.js index eca1284e62e5d..b73a24a275e43 100644 --- a/apps/meteor/.mocharc.api.js +++ b/apps/meteor/.mocharc.api.js @@ -1,13 +1,14 @@ 'use strict'; -/** +/* * Mocha configuration for REST API integration tests. */ -module.exports = { +module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({ ...require('./.mocharc.base.json'), // see https://github.com/mochajs/mocha/issues/3916 timeout: 10000, bail: true, - file: 'tests/end-to-end/teardown.js', + retries: 0, + file: 'tests/end-to-end/teardown.ts', spec: ['tests/end-to-end/api/**/*', 'tests/end-to-end/apps/*'], -}; +}); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 8f44665c204bf..816379df08a92 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -97,7 +97,7 @@ "@types/bcrypt": "^5.0.1", "@types/body-parser": "^1.19.4", "@types/busboy": "^1.5.2", - "@types/chai": "^4.3.9", + "@types/chai": "~4.3.16", "@types/chai-as-promised": "^7.1.7", "@types/chai-datetime": "0.0.38", "@types/chai-dom": "1.11.2", diff --git a/apps/meteor/tests/data/api-data.js b/apps/meteor/tests/data/api-data.js deleted file mode 100644 index cab98c41eb157..0000000000000 --- a/apps/meteor/tests/data/api-data.js +++ /dev/null @@ -1,68 +0,0 @@ -import supertest from 'supertest'; - -import { publicChannelName, privateChannelName } from './channel'; -import { roleNameUsers, roleNameSubscriptions, roleScopeUsers, roleScopeSubscriptions, roleDescription } from './role'; -import { username, email, adminUsername, adminPassword } from './user'; - -const apiUrl = process.env.TEST_API_URL || 'http://localhost:3000'; - -export const request = supertest(apiUrl); -const prefix = '/api/v1/'; - -export function wait(cb, time) { - return () => setTimeout(cb, time); -} - -export const apiUsername = `api${username}-${Date.now()}`; -export const apiEmail = `api${email}-${Date.now()}`; -export const apiPrivateChannelName = `api${privateChannelName}-${Date.now()}`; - -export const apiRoleNameUsers = `api${roleNameUsers}`; -export const apiRoleNameSubscriptions = `api${roleNameSubscriptions}`; -export const apiRoleScopeUsers = `${roleScopeUsers}`; -export const apiRoleScopeSubscriptions = `${roleScopeSubscriptions}`; -export const apiRoleDescription = `api${roleDescription}`; -export const reservedWords = ['admin', 'administrator', 'system', 'user']; - -export const group = {}; -export const message = {}; -export const directMessage = {}; -export const integration = {}; -/** @type {{ 'X-Auth-Token': string | undefined; 'X-User-Id': string | undefined }} */ -export const credentials = { - 'X-Auth-Token': undefined, - 'X-User-Id': undefined, -}; -export const login = { - user: adminUsername, - password: adminPassword, -}; - -export function api(path) { - return prefix + path; -} - -export function methodCall(methodName) { - return api(`method.call/${methodName}`); -} - -export function log(res) { - console.log(res.req.path); - console.log({ - body: res.body, - headers: res.headers, - }); -} - -export function getCredentials(done = function () {}) { - request - .post(api('login')) - .send(login) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - credentials['X-Auth-Token'] = res.body.data.authToken; - credentials['X-User-Id'] = res.body.data.userId; - }) - .end(done); -} diff --git a/apps/meteor/tests/data/api-data.ts b/apps/meteor/tests/data/api-data.ts new file mode 100644 index 0000000000000..f3186752a38f5 --- /dev/null +++ b/apps/meteor/tests/data/api-data.ts @@ -0,0 +1,76 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { Path } from '@rocket.chat/rest-typings'; +import type { CallbackHandler, Response } from 'supertest'; +import supertest from 'supertest'; + +import { adminUsername, adminPassword } from './user'; + +const apiUrl = process.env.TEST_API_URL || 'http://localhost:3000'; + +export const request = supertest(apiUrl); +const prefix = '/api/v1/'; + +export function wait(cb: () => void, time: number) { + return () => setTimeout(cb, time); +} + +const privateChannelName = `private-channel-test-${Date.now()}` as const; + +const username = 'user.test'; +const email = `${username}@rocket.chat`; + +export const apiUsername = `api${username}-${Date.now()}` as const; +export const apiEmail = `api${email}-${Date.now()}` as const; +export const apiPrivateChannelName = `api${privateChannelName}-${Date.now()}` as const; + +const roleNameUsers = `role-name-test-users-${Date.now()}` as const; +const roleNameSubscriptions = `role-name-test-subscriptions-${Date.now()}` as const; +const roleScopeUsers = 'Users' as const; +const roleScopeSubscriptions = 'Subscriptions' as const; +const roleDescription = `role-description-test-${Date.now()}` as const; + +export const apiRoleNameUsers = `api${roleNameUsers}` as const; +export const apiRoleNameSubscriptions = `api${roleNameSubscriptions}` as const; +export const apiRoleScopeUsers = `${roleScopeUsers}` as const; +export const apiRoleScopeSubscriptions = `${roleScopeSubscriptions}` as const; +export const apiRoleDescription = `api${roleDescription}` as const; +export const reservedWords = ['admin', 'administrator', 'system', 'user'] as const; + +export const credentials: Credentials = { + 'X-Auth-Token': undefined, + 'X-User-Id': undefined, +} as unknown as Credentials; // FIXME + +type PathWithoutPrefix = TPath extends `/v1/${infer U}` ? U : never; + +export function api>(path: TPath) { + return `${prefix}${path}` as const; +} + +export function methodCall(methodName: TMethodName) { + return api(`method.call/${methodName}`); +} + +export function log(res: Response) { + console.log((res as { req?: any }).req.path); // FIXME + console.log({ + body: res.body, + headers: res.headers, + }); +} + +export function getCredentials(done?: CallbackHandler) { + void request + .post(api('login')) + .send({ + user: adminUsername, + password: adminPassword, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + credentials['X-Auth-Token'] = res.body.data.authToken; + credentials['X-User-Id'] = res.body.data.userId; + }) + .end(done); +} diff --git a/apps/meteor/tests/data/apps/apps-data.js b/apps/meteor/tests/data/apps/apps-data.js deleted file mode 100644 index bf1e3c4422bd0..0000000000000 --- a/apps/meteor/tests/data/apps/apps-data.js +++ /dev/null @@ -1,4 +0,0 @@ -export const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.5.zip?raw=true'; -export const APP_NAME = 'Apps.RocketChat.Tester'; -export const APP_USERNAME = 'appsrocketchattester.bot'; -export const apps = (path = '') => `/api/apps${path}`; diff --git a/apps/meteor/tests/data/apps/apps-data.ts b/apps/meteor/tests/data/apps/apps-data.ts new file mode 100644 index 0000000000000..6c5117c2150e1 --- /dev/null +++ b/apps/meteor/tests/data/apps/apps-data.ts @@ -0,0 +1,12 @@ +import type { Path } from '@rocket.chat/rest-typings'; + +export const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.5.zip?raw=true'; +export const APP_NAME = 'Apps.RocketChat.Tester'; + +type PathWithoutPrefix = TPath extends `/apps${infer U}` ? U : never; + +export function apps(path?: ''): `/api/apps`; +export function apps>(path: TPath): `/api/apps${TPath}`; +export function apps(path = '') { + return `/api/apps${path}` as const; +} diff --git a/apps/meteor/tests/data/apps/helper.js b/apps/meteor/tests/data/apps/helper.ts similarity index 68% rename from apps/meteor/tests/data/apps/helper.js rename to apps/meteor/tests/data/apps/helper.ts index 787846a1186f5..3236b0652b867 100644 --- a/apps/meteor/tests/data/apps/helper.js +++ b/apps/meteor/tests/data/apps/helper.ts @@ -1,19 +1,21 @@ +import type { App } from '@rocket.chat/core-typings'; + import { request, credentials } from '../api-data'; import { apps, APP_URL, APP_NAME } from './apps-data'; -export const getApps = () => - new Promise((resolve) => { - request +const getApps = () => + new Promise((resolve) => { + void request .get(apps()) .set(credentials) - .end((err, res) => { + .end((_err, res) => { resolve(res.body.apps); }); }); -export const removeAppById = (id) => +const removeAppById = (id: App['id']) => new Promise((resolve) => { - request + void request .delete(apps(`/${id}`)) .set(credentials) .end(resolve); @@ -28,14 +30,14 @@ export const cleanupApps = async () => { }; export const installTestApp = () => - new Promise((resolve) => { - request + new Promise((resolve) => { + void request .post(apps()) .set(credentials) .send({ url: APP_URL, }) - .end((err, res) => { + .end((_err, res) => { resolve(res.body.app); }); }); diff --git a/apps/meteor/tests/data/channel.ts b/apps/meteor/tests/data/channel.ts deleted file mode 100644 index 1cd02fb090228..0000000000000 --- a/apps/meteor/tests/data/channel.ts +++ /dev/null @@ -1 +0,0 @@ -export const privateChannelName = `private-channel-test-${Date.now()}`; diff --git a/apps/meteor/tests/data/chat.helper.js b/apps/meteor/tests/data/chat.helper.ts similarity index 54% rename from apps/meteor/tests/data/chat.helper.js rename to apps/meteor/tests/data/chat.helper.ts index 6e3a2d05a0cdb..46514969bd821 100644 --- a/apps/meteor/tests/data/chat.helper.js +++ b/apps/meteor/tests/data/chat.helper.ts @@ -1,10 +1,24 @@ +import type { IRoom, IMessage } from '@rocket.chat/core-typings'; + import { api, credentials, request } from './api-data'; -export const sendSimpleMessage = ({ roomId, text = 'test message', tmid }) => { +export const sendSimpleMessage = ({ + roomId, + text = 'test message', + tmid, +}: { + roomId: IRoom['_id']; + text?: string; + tmid?: IMessage['_id']; +}) => { if (!roomId) { throw new Error('"roomId" is required in "sendSimpleMessage" test helper'); } - const message = { + const message: { + rid: IRoom['_id']; + text: string; + tmid?: IMessage['_id']; + } = { rid: roomId, text, }; @@ -15,17 +29,7 @@ export const sendSimpleMessage = ({ roomId, text = 'test message', tmid }) => { return request.post(api('chat.sendMessage')).set(credentials).send({ message }); }; -export const pinMessage = ({ msgId }) => { - if (!msgId) { - throw new Error('"msgId" is required in "pinMessage" test helper'); - } - - return request.post(api('chat.pinMessage')).set(credentials).send({ - messageId: msgId, - }); -}; - -export const deleteMessage = ({ roomId, msgId }) => { +export const deleteMessage = ({ roomId, msgId }: { roomId: IRoom['_id']; msgId: IMessage['_id'] }) => { if (!roomId) { throw new Error('"roomId" is required in "deleteMessage" test helper'); } @@ -39,16 +43,17 @@ export const deleteMessage = ({ roomId, msgId }) => { }); }; -export const getMessageById = ({ msgId }) => { +export const getMessageById = ({ msgId }: { msgId: IMessage['_id'] }) => { if (!msgId) { throw new Error('"msgId" is required in "getMessageById" test helper'); } - return new Promise((resolve) => { - request - .get(api(`chat.getMessage?msgId=${msgId}`)) + return new Promise((resolve) => { + void request + .get(api(`chat.getMessage`)) + .query({ msgId }) .set(credentials) - .end((err, res) => { + .end((_err, res) => { resolve(res.body.message); }); }); diff --git a/apps/meteor/tests/data/checks.js b/apps/meteor/tests/data/checks.js deleted file mode 100644 index abfe171915ab4..0000000000000 --- a/apps/meteor/tests/data/checks.js +++ /dev/null @@ -1,15 +0,0 @@ -export let publicChannelCreated = false; -export let privateChannelCreated = false; -export let directMessageCreated = false; - -export function setPublicChannelCreated(status) { - publicChannelCreated = status; -} - -export function setPrivateChannelCreated(status) { - privateChannelCreated = status; -} - -export function setDirectMessageCreated(status) { - directMessageCreated = status; -} diff --git a/apps/meteor/tests/data/constants.ts b/apps/meteor/tests/data/constants.ts index 7cf3c0ba901ec..a7ebc76876db2 100644 --- a/apps/meteor/tests/data/constants.ts +++ b/apps/meteor/tests/data/constants.ts @@ -1,3 +1 @@ export const CI_MAX_ROOMS_PER_GUEST = 10; -export const MAX_BIO_LENGTH = 260; -export const MAX_NICKNAME_LENGTH = 120; diff --git a/apps/meteor/tests/data/custom-fields.js b/apps/meteor/tests/data/custom-fields.js deleted file mode 100644 index e2e175429b4ce..0000000000000 --- a/apps/meteor/tests/data/custom-fields.js +++ /dev/null @@ -1,18 +0,0 @@ -import { credentials, request, api } from './api-data.js'; - -export const customFieldText = { - type: 'text', - required: true, - minLength: 2, - maxLength: 10, -}; - -export function setCustomFields(customFields) { - const stringified = customFields ? JSON.stringify(customFields) : ''; - - return request.post(api('settings/Accounts_CustomFields')).set(credentials).send({ value: stringified }).expect(200); -} - -export function clearCustomFields() { - return setCustomFields(null); -} diff --git a/apps/meteor/tests/data/integration.helper.js b/apps/meteor/tests/data/integration.helper.ts similarity index 54% rename from apps/meteor/tests/data/integration.helper.js rename to apps/meteor/tests/data/integration.helper.ts index 6a28643ffc7b9..211de64b6e1ac 100644 --- a/apps/meteor/tests/data/integration.helper.js +++ b/apps/meteor/tests/data/integration.helper.ts @@ -1,8 +1,12 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IIntegration } from '@rocket.chat/core-typings'; +import type { IntegrationsCreateProps } from '@rocket.chat/rest-typings'; + import { api, credentials, request } from './api-data'; -export const createIntegration = (integration, userCredentials) => - new Promise((resolve, reject) => { - request +export const createIntegration = (integration: IntegrationsCreateProps, userCredentials: Credentials) => + new Promise((resolve, reject) => { + void request .post(api('integrations.create')) .set(userCredentials) .send(integration) @@ -21,9 +25,9 @@ export const createIntegration = (integration, userCredentials) => }); }); -export const removeIntegration = (integrationId, type) => - new Promise((resolve, reject) => { - request +export const removeIntegration = (integrationId: IIntegration['_id'], type: 'incoming' | 'outgoing') => + new Promise((resolve) => { + void request .post(api('integrations.remove')) .set(credentials) .send({ diff --git a/apps/meteor/tests/data/interactions.ts b/apps/meteor/tests/data/interactions.ts index 085d97d4ece38..9d5a69ce95935 100644 --- a/apps/meteor/tests/data/interactions.ts +++ b/apps/meteor/tests/data/interactions.ts @@ -1,6 +1 @@ -export const targetUser = 'rocket.cat'; export const imgURL = './public/images/logo/1024x1024.png'; -export const lstURL = './tests/e2e/fixtures/files/lst-test.lst'; -export const drawioURL = './tests/e2e/fixtures/files/diagram.drawio'; -export const svgLogoURL = './public/images/logo/logo.svg'; -export const svgLogoFileName = 'logo.svg'; diff --git a/apps/meteor/tests/data/licenses.helper.ts b/apps/meteor/tests/data/licenses.helper.ts deleted file mode 100644 index 61177ee549d33..0000000000000 --- a/apps/meteor/tests/data/licenses.helper.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { api, credentials, request } from "./api-data" - -export const getLicenseInfo = (loadValues = false) => { - return request.get(api('licenses.info')).set(credentials).query({ loadValues }).expect(200); -} \ No newline at end of file diff --git a/apps/meteor/tests/data/livechat/businessHours.ts b/apps/meteor/tests/data/livechat/businessHours.ts index 58d8affa3d2a0..ad835b0cff257 100644 --- a/apps/meteor/tests/data/livechat/businessHours.ts +++ b/apps/meteor/tests/data/livechat/businessHours.ts @@ -1,48 +1,50 @@ -import { ILivechatBusinessHour, LivechatBusinessHourTypes } from "@rocket.chat/core-typings"; -import { api, credentials, methodCall, request } from "../api-data"; -import { updateEESetting, updateSetting } from "../permissions.helper" -import moment from "moment"; +import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; +import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import moment from 'moment'; -type ISaveBhApiWorkHour = Omit & { workHours: { day: string, start: string, finish: string, open: boolean }[] } & { departmentsToApplyBusinessHour?: string } & { timezoneName: string }; +import { api, credentials, methodCall, request } from '../api-data'; +import { updateEESetting, updateSetting } from '../permissions.helper'; + +type ISaveBhApiWorkHour = Omit & { + workHours: { day: string; start: string; finish: string; open: boolean }[]; +} & { departmentsToApplyBusinessHour?: string } & { timezoneName: string }; // TODO: Migrate to an API call and return the business hour updated/created export const saveBusinessHour = async (businessHour: ISaveBhApiWorkHour) => { - const { body } = await request - .post(methodCall('livechat:saveBusinessHour')) - .set(credentials) - .send({ message: JSON.stringify({ params: [businessHour], msg: 'method', method: 'livechat:saveBusinessHour', id: '101' }) }) - .expect(200); + const { body } = await request + .post(methodCall('livechat:saveBusinessHour')) + .set(credentials) + .send({ message: JSON.stringify({ params: [businessHour], msg: 'method', method: 'livechat:saveBusinessHour', id: '101' }) }) + .expect(200); - return JSON.parse(body.message); + return JSON.parse(body.message); }; export const createCustomBusinessHour = async (departments: string[], open = true): Promise => { - const name = `business-hour-${Date.now()}`; - const businessHour: ISaveBhApiWorkHour = { - name, - active: true, - type: LivechatBusinessHourTypes.CUSTOM, - workHours: getWorkHours(open), - timezoneName: 'Asia/Calcutta', - departmentsToApplyBusinessHour: '', - }; - - if (departments.length) { - businessHour.departmentsToApplyBusinessHour = departments.join(','); - } - - await saveBusinessHour(businessHour); - - - const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours(); - const createdBusinessHour = existingBusinessHours.find((bh) => bh.name === name); - if (!createdBusinessHour) { - throw new Error('Could not create business hour'); - } - - return createdBusinessHour; -}; + const name = `business-hour-${Date.now()}`; + const businessHour: ISaveBhApiWorkHour = { + name, + active: true, + type: LivechatBusinessHourTypes.CUSTOM, + workHours: getWorkHours(open), + timezoneName: 'Asia/Calcutta', + departmentsToApplyBusinessHour: '', + }; + + if (departments.length) { + businessHour.departmentsToApplyBusinessHour = departments.join(','); + } + await saveBusinessHour(businessHour); + + const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours(); + const createdBusinessHour = existingBusinessHours.find((bh) => bh.name === name); + if (!createdBusinessHour) { + throw new Error('Could not create business hour'); + } + + return createdBusinessHour; +}; export const makeDefaultBusinessHourActiveAndClosed = async () => { // enable settings @@ -50,13 +52,12 @@ export const makeDefaultBusinessHourActiveAndClosed = async () => { await updateEESetting('Livechat_business_hour_type', 'Single'); // create business hours - const { body: { businessHour } } = await request - .get(api('livechat/business-hour?type=default')) - .set(credentials) - .send(); + const { + body: { businessHour }, + } = await request.get(api('livechat/business-hour')).query({ type: 'default' }).set(credentials).send(); - // TODO: Refactor this to use openOrCloseBusinessHour() instead - const workHours = businessHour.workHours as { start: string; finish: string; day: string, open: boolean }[]; + // TODO: Refactor this to use openOrCloseBusinessHour() instead + const workHours = businessHour.workHours as { start: string; finish: string; day: string; open: boolean }[]; const allEnabledWorkHours = workHours.map((workHour) => { workHour.open = true; workHour.start = '00:00'; @@ -67,9 +68,10 @@ export const makeDefaultBusinessHourActiveAndClosed = async () => { const enabledBusinessHour = { ...businessHour, workHours: allEnabledWorkHours, - } + }; - await request.post(methodCall('livechat:saveBusinessHour')) + await request + .post(methodCall('livechat:saveBusinessHour')) .set(credentials) .send({ message: JSON.stringify({ @@ -79,7 +81,7 @@ export const makeDefaultBusinessHourActiveAndClosed = async () => { msg: 'method', }), }); -} +}; export const disableDefaultBusinessHour = async () => { // disable settings @@ -87,13 +89,12 @@ export const disableDefaultBusinessHour = async () => { await updateEESetting('Livechat_business_hour_type', 'Single'); // create business hours - const { body: { businessHour } } = await request - .get(api('livechat/business-hour?type=default')) - .set(credentials) - .send(); + const { + body: { businessHour }, + } = await request.get(api('livechat/business-hour')).query({ type: 'default' }).set(credentials).send(); - // TODO: Refactor this to use openOrCloseBusinessHour() instead - const workHours = businessHour.workHours as { start: string; finish: string; day: string, open: boolean }[]; + // TODO: Refactor this to use openOrCloseBusinessHour() instead + const workHours = businessHour.workHours as { start: string; finish: string; day: string; open: boolean }[]; const allDisabledWorkHours = workHours.map((workHour) => { workHour.open = false; workHour.start = '00:00'; @@ -104,9 +105,10 @@ export const disableDefaultBusinessHour = async () => { const disabledBusinessHour = { ...businessHour, workHours: allDisabledWorkHours, - } + }; - await request.post(methodCall('livechat:saveBusinessHour')) + await request + .post(methodCall('livechat:saveBusinessHour')) .set(credentials) .send({ message: JSON.stringify({ @@ -116,67 +118,83 @@ export const disableDefaultBusinessHour = async () => { msg: 'method', }), }); -} - -export const removeCustomBusinessHour = async (businessHourId: string) => { - await request - .post(methodCall('livechat:removeBusinessHour')) - .set(credentials) - .send({ message: JSON.stringify({ params: [businessHourId, LivechatBusinessHourTypes.CUSTOM], msg: 'method', method: 'livechat:removeBusinessHour', id: '101' }) }) - .expect(200); }; -const getAllCustomBusinessHours = async (): Promise => { - const response = await request.get(api('livechat/business-hours')).set(credentials).expect(200); - return (response.body.businessHours || []).filter((businessHour: ILivechatBusinessHour) => businessHour.type === LivechatBusinessHourTypes.CUSTOM); +const removeCustomBusinessHour = async (businessHourId: string) => { + await request + .post(methodCall('livechat:removeBusinessHour')) + .set(credentials) + .send({ + message: JSON.stringify({ + params: [businessHourId, LivechatBusinessHourTypes.CUSTOM], + msg: 'method', + method: 'livechat:removeBusinessHour', + id: '101', + }), + }) + .expect(200); }; +const getAllCustomBusinessHours = async (): Promise => { + const response = await request.get(api('livechat/business-hours')).set(credentials).expect(200); + return (response.body.businessHours || []).filter( + (businessHour: ILivechatBusinessHour) => businessHour.type === LivechatBusinessHourTypes.CUSTOM, + ); +}; export const removeAllCustomBusinessHours = async () => { - const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours(); + const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours(); - const promises = existingBusinessHours.map((businessHour) => removeCustomBusinessHour(businessHour._id)); - await Promise.all(promises); + const promises = existingBusinessHours.map((businessHour) => removeCustomBusinessHour(businessHour._id)); + await Promise.all(promises); }; export const getDefaultBusinessHour = async (): Promise => { - const response = await request.get(api('livechat/business-hour')).set(credentials).query({ type: LivechatBusinessHourTypes.DEFAULT }).expect(200); - return response.body.businessHour; + const response = await request + .get(api('livechat/business-hour')) + .set(credentials) + .query({ type: LivechatBusinessHourTypes.DEFAULT }) + .expect(200); + return response.body.businessHour; }; export const getCustomBusinessHourById = async (businessHourId: string): Promise => { - const response = await request.get(api('livechat/business-hour')).set(credentials).query({ type: LivechatBusinessHourTypes.CUSTOM, _id: businessHourId }).expect(200); - return response.body.businessHour; + const response = await request + .get(api('livechat/business-hour')) + .set(credentials) + .query({ type: LivechatBusinessHourTypes.CUSTOM, _id: businessHourId }) + .expect(200); + return response.body.businessHour; }; export const openOrCloseBusinessHour = async (businessHour: ILivechatBusinessHour, open: boolean) => { - const enabledBusinessHour = { - ...businessHour, - timezoneName: businessHour.timezone.name, - workHours: getWorkHours().map((workHour) => { - return { - ...workHour, - open, - } - }), - departmentsToApplyBusinessHour: businessHour.departments?.map((department) => department._id).join(',') || '', - } - - await saveBusinessHour(enabledBusinessHour as any); -} + const enabledBusinessHour = { + ...businessHour, + timezoneName: businessHour.timezone.name, + workHours: getWorkHours().map((workHour) => { + return { + ...workHour, + open, + }; + }), + departmentsToApplyBusinessHour: businessHour.departments?.map((department) => department._id).join(',') || '', + }; + + await saveBusinessHour(enabledBusinessHour as any); +}; export const getWorkHours = (open = true): ISaveBhApiWorkHour['workHours'] => { - const workHours: ISaveBhApiWorkHour['workHours'] = []; + const workHours: ISaveBhApiWorkHour['workHours'] = []; - for (let i = 0; i < 7; i++) { - workHours.push({ - day: moment().day(i).format('dddd'), - start: '00:00', - finish: '23:59', + for (let i = 0; i < 7; i++) { + workHours.push({ + day: moment().day(i).format('dddd'), + start: '00:00', + finish: '23:59', - open, - }); - } + open, + }); + } - return workHours; -} + return workHours; +}; diff --git a/apps/meteor/tests/data/livechat/canned-responses.ts b/apps/meteor/tests/data/livechat/canned-responses.ts index 7d56e0957e9fe..23cfac13e2369 100644 --- a/apps/meteor/tests/data/livechat/canned-responses.ts +++ b/apps/meteor/tests/data/livechat/canned-responses.ts @@ -1,24 +1,25 @@ import { faker } from '@faker-js/faker'; import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; + import { api, credentials, request } from '../api-data'; import type { DummyResponse } from './utils'; export const createCannedResponse = (): Promise> => new Promise((resolve, reject) => { - const response = { - shortcut: `${faker.lorem.word()}-${Date.now()}`, - scope: 'user', - tags: [faker.lorem.word()], - text: faker.lorem.sentence(), - }; + const response = { + shortcut: `${faker.lorem.word()}-${Date.now()}`, + scope: 'user', + tags: [faker.lorem.word()], + text: faker.lorem.sentence(), + }; return request .post(api(`canned-responses`)) .set(credentials) - .send(response) + .send(response) .end((_err: Error, _res: DummyResponse) => { - if (_err) { - return reject(_err); - } - resolve(response); - }); + if (_err) { + return reject(_err); + } + resolve(response); + }); }); diff --git a/apps/meteor/tests/data/livechat/custom-fields.ts b/apps/meteor/tests/data/livechat/custom-fields.ts index f41fcf41115f3..f4878f6f754d6 100644 --- a/apps/meteor/tests/data/livechat/custom-fields.ts +++ b/apps/meteor/tests/data/livechat/custom-fields.ts @@ -1,60 +1,61 @@ -import type { Response } from 'supertest'; import type { ILivechatCustomField } from '@rocket.chat/core-typings'; -import { credentials, request, methodCall, api } from './../api-data'; - -type ExtendedCustomField = Omit & { field: string }; +import type { Response } from 'supertest'; -export const createCustomField = (customField: ExtendedCustomField): Promise => new Promise((resolve, reject) => { - request - .get(api(`livechat/custom-fields/${customField.label}`)) - .set(credentials) - .send() - .end((err: Error, res: Response) => { - if (err) { - return reject(err); - } - if (res.body.customField != null && res.body.customField != undefined) { - resolve(res.body.customField); - } else { - request - .post(methodCall('livechat:saveCustomField')) - .send({ - message: JSON.stringify({ - method: 'livechat:saveCustomField', - params: [null, customField], - id: 'id', - msg: 'method', - }), - }) - .set(credentials) - .end((err: Error, res: Response): void => { - if (err) { - return reject(err); - } - resolve(res.body); - }); - } - }); +import { credentials, request, methodCall, api } from '../api-data'; -}); +type ExtendedCustomField = Omit & { field: string }; -export const deleteCustomField = (customFieldID: string) => new Promise((resolve, reject) => { - request - .post(methodCall('livechat:removeCustomField')) - .send({ - message: JSON.stringify({ - method: 'livechat:removeCustomField', - params: [customFieldID], - id: 'id', - msg: 'method', - }), - }) - .set(credentials) - .end((err: Error, res: Response): void => { - if (err) { - return reject(err); - } - resolve(res.body); - }); -}); +export const createCustomField = (customField: ExtendedCustomField): Promise => + new Promise((resolve, reject) => { + void request + .get(api(`livechat/custom-fields/${customField.label}`)) + .set(credentials) + .send() + .end((err: Error, res: Response) => { + if (err) { + return reject(err); + } + if (res.body.customField !== null && res.body.customField !== undefined) { + resolve(res.body.customField); + } else { + void request + .post(methodCall('livechat:saveCustomField')) + .send({ + message: JSON.stringify({ + method: 'livechat:saveCustomField', + params: [null, customField], + id: 'id', + msg: 'method', + }), + }) + .set(credentials) + .end((err: Error, res: Response): void => { + if (err) { + return reject(err); + } + resolve(res.body); + }); + } + }); + }); +export const deleteCustomField = (customFieldID: string) => + new Promise((resolve, reject) => { + void request + .post(methodCall('livechat:removeCustomField')) + .send({ + message: JSON.stringify({ + method: 'livechat:removeCustomField', + params: [customFieldID], + id: 'id', + msg: 'method', + }), + }) + .set(credentials) + .end((err: Error, res: Response): void => { + if (err) { + return reject(err); + } + resolve(res.body); + }); + }); diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts index ba0df137b5678..47d0f7f2b4680 100644 --- a/apps/meteor/tests/data/livechat/department.ts +++ b/apps/meteor/tests/data/livechat/department.ts @@ -1,78 +1,85 @@ import { faker } from '@faker-js/faker'; -import { expect } from 'chai'; +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatDepartment, IUser, LivechatDepartmentDTO } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; + import { api, credentials, methodCall, request } from '../api-data'; -import { IUserCredentialsHeader } from '../user'; import { createAnOnlineAgent, createAnOfflineAgent } from './users'; -import { WithRequiredProperty } from './utils'; - -export const NewDepartmentData = ((): Partial => ({ - enabled: true, - name: `new department ${Date.now()}`, - description: 'created from api', - showOnRegistration: true, - email: faker.internet.email(), - showOnOfflineForm: true, +import type { WithRequiredProperty } from './utils'; + +const NewDepartmentData = ((): Partial => ({ + enabled: true, + name: `new department ${Date.now()}`, + description: 'created from api', + showOnRegistration: true, + email: faker.internet.email(), + showOnOfflineForm: true, }))(); export const createDepartment = async (departmentData: Partial = NewDepartmentData): Promise => { - const response = await request.post(api('livechat/department')).set(credentials).send({ - department: departmentData, - }).expect(200); - return response.body.department; -}; - -export const updateDepartment = async (departmentId: string, departmentData: Partial): Promise => { - const response = await request.put(api(`livechat/department/${ departmentId }`)).set(credentials).send({ - department: departmentData, - }).expect(200); - return response.body.department; + const response = await request + .post(api('livechat/department')) + .set(credentials) + .send({ + department: departmentData, + }) + .expect(200); + return response.body.department; }; -export const createDepartmentWithMethod = ( - initialAgents: { agentId: string, username: string }[] = [], - allowReceiveForwardOffline = false) => -new Promise((resolve, reject) => { - request - .post(methodCall('livechat:saveDepartment')) +const updateDepartment = async (departmentId: string, departmentData: Partial): Promise => { + const response = await request + .put(api(`livechat/department/${departmentId}`)) .set(credentials) .send({ - message: JSON.stringify({ - method: 'livechat:saveDepartment', - params: [ - '', - { - enabled: true, - email: faker.internet.email(), - showOnRegistration: true, - showOnOfflineForm: true, - name: `new department ${Date.now()}`, - description: 'created from api', - allowReceiveForwardOffline, - }, - initialAgents, - ], - id: 'id', - msg: 'method', - }), + department: departmentData, }) - .end((err: any, res: any) => { - if (err) { - return reject(err); - } - resolve(JSON.parse(res.body.message).result); - }); -}); + .expect(200); + return response.body.department; +}; + +const createDepartmentWithMethod = (initialAgents: { agentId: string; username: string }[] = [], allowReceiveForwardOffline = false) => + new Promise((resolve, reject) => { + void request + .post(methodCall('livechat:saveDepartment')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveDepartment', + params: [ + '', + { + enabled: true, + email: faker.internet.email(), + showOnRegistration: true, + showOnOfflineForm: true, + name: `new department ${Date.now()}`, + description: 'created from api', + allowReceiveForwardOffline, + }, + initialAgents, + ], + id: 'id', + msg: 'method', + }), + }) + .end((err: any, res: any) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); type OnlineAgent = { user: WithRequiredProperty; - credentials: IUserCredentialsHeader; + credentials: Credentials; }; -export const createDepartmentWithAnOnlineAgent = async (): Promise<{department: ILivechatDepartment, agent: OnlineAgent }> => { - const { user, credentials } = await createAnOnlineAgent(); +export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => { + const { user, credentials } = await createAnOnlineAgent(); - const department = await createDepartmentWithMethod() as ILivechatDepartment; + const department = (await createDepartmentWithMethod()) as ILivechatDepartment; await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true); @@ -81,13 +88,13 @@ export const createDepartmentWithAnOnlineAgent = async (): Promise<{department: agent: { credentials, user, - } + }, }; }; export const createDepartmentWithAgent = async (agent: OnlineAgent): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => { const { user, credentials } = agent; - const department = await createDepartmentWithMethod() as ILivechatDepartment; + const department = (await createDepartmentWithMethod()) as ILivechatDepartment; await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true); @@ -96,19 +103,27 @@ export const createDepartmentWithAgent = async (agent: OnlineAgent): Promise<{ d agent: { credentials, user, - } + }, }; -} +}; -export const addOrRemoveAgentFromDepartment = async (departmentId: string, agent: { agentId: string; username: string; count?: number; order?: number }, add: boolean) => { - const response = await request.post(api('livechat/department/' + departmentId + '/agents')).set(credentials).send({ - ...add ? { upsert: [agent], remove: [] } : { remove: [agent], upsert: [] }, - }); +export const addOrRemoveAgentFromDepartment = async ( + departmentId: string, + agent: { agentId: string; username: string; count?: number; order?: number }, + add: boolean, +) => { + const response = await request + .post(api(`livechat/department/${departmentId}/agents`)) + .set(credentials) + .send({ + ...(add ? { upsert: [agent], remove: [] } : { remove: [agent], upsert: [] }), + }); if (response.status !== 200) { - throw new Error('Failed to add or remove agent from department. Status code: ' + response.status + '\n' + response.body); + throw new Error(`Failed to add or remove agent from department. Status code: ${response.status}\n${response.body}`); } -} +}; + export const createDepartmentWithAnOfflineAgent = async ({ allowReceiveForwardOffline = false, }: { @@ -116,7 +131,7 @@ export const createDepartmentWithAnOfflineAgent = async ({ }): Promise<{ department: ILivechatDepartment; agent: { - credentials: IUserCredentialsHeader; + credentials: Credentials; user: WithRequiredProperty; }; }> => { @@ -136,21 +151,30 @@ export const createDepartmentWithAnOfflineAgent = async ({ }; export const archiveDepartment = async (departmentId: string): Promise => { - await request.post(api(`livechat/department/${ departmentId }/archive`)).set(credentials).expect(200); -} + await request + .post(api(`livechat/department/${departmentId}/archive`)) + .set(credentials) + .expect(200); +}; export const disableDepartment = async (department: ILivechatDepartment): Promise => { - department.enabled = false; - delete department._updatedAt; - const updatedDepartment = await updateDepartment(department._id, department); - expect(updatedDepartment.enabled).to.be.false; -} + department.enabled = false; + delete department._updatedAt; + const updatedDepartment = await updateDepartment(department._id, department); + expect(updatedDepartment.enabled).to.be.false; +}; export const deleteDepartment = async (departmentId: string): Promise => { - await request.delete(api(`livechat/department/${ departmentId }`)).set(credentials).expect(200); -} + await request + .delete(api(`livechat/department/${departmentId}`)) + .set(credentials) + .expect(200); +}; export const getDepartmentById = async (departmentId: string): Promise => { - const response = await request.get(api(`livechat/department/${ departmentId }`)).set(credentials).expect(200); - return response.body.department; + const response = await request + .get(api(`livechat/department/${departmentId}`)) + .set(credentials) + .expect(200); + return response.body.department; }; diff --git a/apps/meteor/tests/data/livechat/inboxes.ts b/apps/meteor/tests/data/livechat/inboxes.ts index ccc7c742da1aa..7def060d3cb33 100644 --- a/apps/meteor/tests/data/livechat/inboxes.ts +++ b/apps/meteor/tests/data/livechat/inboxes.ts @@ -1,31 +1,31 @@ import { getCredentials, api, request, credentials } from '../api-data'; export const createEmailInbox = async (): Promise<{ _id: string }> => { - await getCredentials() - const { body } = await request - .post(api('email-inbox')) - .set(credentials) - .send({ - name: 'test', - active: false, - email: `test${new Date().getTime()}@test.com`, - description: 'test', - senderInfo: 'test', - smtp: { - server: 'smtp.example.com', - port: 587, - username: 'xxxx', - password: 'xxxx', - secure: true, - }, - imap: { - server: 'imap.example.com', - port: 993, - username: 'xxxx', - password: 'xxxx', - secure: true, - maxRetries: 10, - }, - }); - return body; -} + await new Promise((resolve) => getCredentials(resolve)); + const { body } = await request + .post(api('email-inbox')) + .set(credentials) + .send({ + name: 'test', + active: false, + email: `test${new Date().getTime()}@test.com`, + description: 'test', + senderInfo: 'test', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'xxxx', + password: 'xxxx', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'xxxx', + password: 'xxxx', + secure: true, + maxRetries: 10, + }, + }); + return body; +}; diff --git a/apps/meteor/tests/data/livechat/inquiries.ts b/apps/meteor/tests/data/livechat/inquiries.ts index b108e1d4d80ea..94f4ce295556e 100644 --- a/apps/meteor/tests/data/livechat/inquiries.ts +++ b/apps/meteor/tests/data/livechat/inquiries.ts @@ -1,9 +1,11 @@ +/* eslint-disable no-await-in-loop */ +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; -import { api, request } from '../api-data'; +import { api, request } from '../api-data'; -export const fetchAllInquiries = async (credentials: { 'X-Auth-Token': string; 'X-User-Id': string; }, department?: string): Promise => { +export const fetchAllInquiries = async (credentials: Credentials, department?: string): Promise => { const inquiries: ILivechatInquiryRecord[] = []; let hasMore = true; @@ -26,6 +28,5 @@ export const fetchAllInquiries = async (credentials: { 'X-Auth-Token': string; ' offset += body.count; } - return inquiries; -} +}; diff --git a/apps/meteor/tests/data/livechat/priorities.ts b/apps/meteor/tests/data/livechat/priorities.ts index 32f8e53352342..e77845f614ec4 100644 --- a/apps/meteor/tests/data/livechat/priorities.ts +++ b/apps/meteor/tests/data/livechat/priorities.ts @@ -1,12 +1,13 @@ -import { api, credentials, request } from '../api-data'; -import type { DummyResponse } from './utils'; +import type { ILivechatPriority, IOmnichannelServiceLevelAgreements } from '@rocket.chat/core-typings'; import { expect } from 'chai'; + import { generateRandomSLAData } from '../../e2e/utils/omnichannel/sla'; -import type { ILivechatPriority, IOmnichannelServiceLevelAgreements } from '@rocket.chat/core-typings'; +import { api, credentials, request } from '../api-data'; +import type { DummyResponse } from './utils'; export const createSLA = (): Promise> => { return new Promise((resolve, reject) => { - request + void request .post(api('livechat/sla')) .set(credentials) .send(generateRandomSLAData()) @@ -21,7 +22,7 @@ export const createSLA = (): Promise => { return new Promise((resolve, reject) => { - request + void request .delete(api(`livechat/sla/${id}`)) .set(credentials) .send() @@ -67,4 +68,4 @@ export const getRandomPriority = async (): Promise => { body: { priorities }, } = response as { body: { priorities: ILivechatPriority[] } }; return priorities[Math.floor(Math.random() * priorities.length)]; -} +}; diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 921e7bb08582d..e2084adda9347 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -1,4 +1,6 @@ +/* eslint-disable no-await-in-loop */ import { faker } from '@faker-js/faker'; +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatInquiryRecord, ILivechatAgent, @@ -7,13 +9,15 @@ import type { IMessage, IOmnichannelRoom, } from '@rocket.chat/core-typings'; +import type { Response } from 'supertest'; + import { api, credentials, methodCall, request } from '../api-data'; +import { imgURL } from '../interactions'; import { getSettingValueById, restorePermissionToRoles, updateSetting } from '../permissions.helper'; -import { IUserCredentialsHeader, adminUsername } from '../user'; +import { adminUsername } from '../user'; import { getRandomVisitorToken } from './users'; -import { DummyResponse, sleep } from './utils'; -import { Response } from 'supertest'; -import { imgURL } from '../interactions'; +import type { DummyResponse } from './utils'; +import { sleep } from './utils'; export const createLivechatRoom = async (visitorToken: string, extraRoomParams?: Record): Promise => { const urlParams = new URLSearchParams(); @@ -24,10 +28,7 @@ export const createLivechatRoom = async (visitorToken: string, extraRoomParams?: } } - const response = await request - .get(api(`livechat/room?${urlParams.toString()}`)) - .set(credentials) - .expect(200); + const response = await request.get(api('livechat/room')).query(urlParams.toString()).set(credentials).expect(200); return response.body.room; }; @@ -37,11 +38,11 @@ export const createVisitor = (department?: string, visitorName?: string): Promis const token = getRandomVisitorToken(); const email = `${token}@${token}.com`; const phone = `${Math.floor(Math.random() * 10000000000)}`; - request.get(api(`livechat/visitor/${token}`)).end((err: Error, res: DummyResponse) => { + void request.get(api(`livechat/visitor/${token}`)).end((err: Error, res: DummyResponse) => { if (!err && res && res.body && res.body.visitor) { return resolve(res.body.visitor); } - request + void request .post(api('livechat/visitor')) .set(credentials) .send({ @@ -65,18 +66,23 @@ export const createVisitor = (department?: string, visitorName?: string): Promis export const deleteVisitor = async (token: string): Promise => { await request.delete(api(`livechat/visitor/${token}`)); -} +}; -export const takeInquiry = async (inquiryId: string, agentCredentials?: IUserCredentialsHeader): Promise => { - const userId = agentCredentials ? agentCredentials['X-User-Id'] : credentials['X-User-Id']; +export const takeInquiry = async (inquiryId: string, agentCredentials?: Credentials): Promise => { + const userId = agentCredentials ? agentCredentials['X-User-Id'] : credentials['X-User-Id']; - await request.post(api('livechat/inquiries.take')).set(agentCredentials || credentials).send({ userId, inquiryId }).expect(200); + await request + .post(api('livechat/inquiries.take')) + .set(agentCredentials || credentials) + .send({ userId, inquiryId }) + .expect(200); }; export const fetchInquiry = (roomId: string): Promise => { return new Promise((resolve, reject) => { - request - .get(api(`livechat/inquiries.getOne?roomId=${roomId}`)) + void request + .get(api('livechat/inquiries.getOne')) + .query({ roomId }) .set(credentials) .end((err: Error, res: DummyResponse) => { if (err) { @@ -87,9 +93,14 @@ export const fetchInquiry = (roomId: string): Promise => }); }; -export const createDepartment = (agents?: { agentId: string }[], name?: string, enabled = true, opts: Record = {}): Promise => { +export const createDepartment = ( + agents?: { agentId: string }[], + name?: string, + enabled = true, + opts: Record = {}, +): Promise => { return new Promise((resolve, reject) => { - request + void request .post(api('livechat/department')) .set(credentials) .send({ @@ -114,7 +125,7 @@ export const createDepartment = (agents?: { agentId: string }[], name?: string, export const createAgent = (overrideUsername?: string): Promise => new Promise((resolve, reject) => { - request + void request .post(api('livechat/users/agent')) .set(credentials) .send({ @@ -130,7 +141,7 @@ export const createAgent = (overrideUsername?: string): Promise export const createManager = (overrideUsername?: string): Promise => new Promise((resolve, reject) => { - request + void request .post(api('livechat/users/manager')) .set(credentials) .send({ @@ -144,7 +155,7 @@ export const createManager = (overrideUsername?: string): Promise => { +export const makeAgentAvailable = async (overrideCredentials?: Credentials): Promise => { await restorePermissionToRoles('view-l-room'); await request .post(api('users.setStatus')) @@ -162,7 +173,7 @@ export const makeAgentAvailable = async (overrideCredentials?: { 'X-Auth-Token': }); }; -export const makeAgentUnavailable = async (overrideCredentials?: { 'X-Auth-Token': string; 'X-User-Id': string }): Promise => { +export const makeAgentUnavailable = async (overrideCredentials?: Credentials): Promise => { await request .post(api('users.setStatus')) .set(overrideCredentials || credentials) @@ -179,7 +190,7 @@ export const makeAgentUnavailable = async (overrideCredentials?: { 'X-Auth-Token export const getLivechatRoomInfo = (roomId: string): Promise => { return new Promise((resolve /* , reject*/) => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -193,10 +204,10 @@ export const getLivechatRoomInfo = (roomId: string): Promise = /** * @summary Sends message as visitor -*/ + */ export const sendMessage = (roomId: string, message: string, visitorToken: string): Promise => { return new Promise((resolve, reject) => { - request + void request .post(api('livechat/message')) .set(credentials) .send({ @@ -215,7 +226,7 @@ export const sendMessage = (roomId: string, message: string, visitorToken: strin export const uploadFile = (roomId: string, visitorToken: string): Promise => { return new Promise((resolve, reject) => { - request + void request .post(api(`livechat/upload/${roomId}`)) .set({ 'x-visitor-token': visitorToken, ...credentials }) .attach('file', imgURL) @@ -231,7 +242,7 @@ export const uploadFile = (roomId: string, visitorToken: string): Promise => { return new Promise((resolve, reject) => { - request + void request .post(methodCall('sendMessage')) .set(credentials) .send({ @@ -253,7 +264,7 @@ export const sendAgentMessage = (roomId: string, msg?: string): Promise => { return new Promise((resolve, reject) => { - request + void request .get(api(`livechat/messages.history/${roomId}`)) .set(credentials) .query({ @@ -275,7 +286,11 @@ export const fetchMessages = (roomId: string, visitorToken: string): Promise => { - await request.post(api('livechat/room.closeByUser')).set(credentials).send({ rid: roomId, ...tags && { tags }, comment: faker.lorem.sentence() }).expect(200); + await request + .post(api('livechat/room.closeByUser')) + .set(credentials) + .send({ rid: roomId, ...(tags && { tags }), comment: faker.lorem.sentence() }) + .expect(200); }; export const bulkCreateLivechatRooms = async ( @@ -298,22 +313,20 @@ export const bulkCreateLivechatRooms = async ( }; export const startANewLivechatRoomAndTakeIt = async ({ - departmentId, - agent + departmentId, + agent, }: { - departmentId?: string; - agent?: IUserCredentialsHeader; + departmentId?: string; + agent?: Credentials; } = {}): Promise<{ room: IOmnichannelRoom; visitor: ILivechatVisitor }> => { + const currentRoutingMethod = await getSettingValueById('Livechat_Routing_Method'); + const routingMethodChanged = false; + if (currentRoutingMethod !== 'Manual_Selection') { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); - const currentRoutingMethod = await getSettingValueById('Livechat_Routing_Method'); - let routingMethodChanged = false; - if (currentRoutingMethod !== 'Manual_Selection') { - await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); - - // wait for routing algorithm to stop - await sleep(1000); - } - + // wait for routing algorithm to stop + await sleep(1000); + } const visitor = await createVisitor(departmentId); const room = await createLivechatRoom(visitor.token); @@ -322,26 +335,21 @@ export const startANewLivechatRoomAndTakeIt = async ({ await takeInquiry(inq._id, agent); await sendMessage(roomId, 'test message', visitor.token); + if (routingMethodChanged) { + await updateSetting('Livechat_Routing_Method', currentRoutingMethod); - if (routingMethodChanged) { - await updateSetting('Livechat_Routing_Method', currentRoutingMethod); - - // wait for routing algorithm to start - await sleep(1000); - } + // wait for routing algorithm to start + await sleep(1000); + } return { room, visitor }; }; export const placeRoomOnHold = async (roomId: string): Promise => { - await request - .post(api('livechat/room.onHold')) - .set(credentials) - .send({ roomId }) - .expect(200); -} - -export const moveBackToQueue = async (roomId: string, overrideCredentials?: IUserCredentialsHeader): Promise => { + await request.post(api('livechat/room.onHold')).set(credentials).send({ roomId }).expect(200); +}; + +export const moveBackToQueue = async (roomId: string, overrideCredentials?: Credentials): Promise => { await request .post(methodCall('livechat:returnAsInquiry')) .set(overrideCredentials || credentials) diff --git a/apps/meteor/tests/data/livechat/tags.ts b/apps/meteor/tests/data/livechat/tags.ts index b12cca1f64ae2..5f5cac0e40391 100644 --- a/apps/meteor/tests/data/livechat/tags.ts +++ b/apps/meteor/tests/data/livechat/tags.ts @@ -1,17 +1,18 @@ import { faker } from '@faker-js/faker'; import type { ILivechatTag } from '@rocket.chat/core-typings'; + import { credentials, methodCall, request } from '../api-data'; import type { DummyResponse } from './utils'; export const saveTags = (departments: string[] = []): Promise => { - return new Promise((resolve, reject) => { - request + return new Promise((resolve, reject) => { + void request .post(methodCall(`livechat:saveTag`)) .set(credentials) .send({ message: JSON.stringify({ method: 'livechat:saveTag', - params: [undefined, { name: faker.person.firstName(), description: faker.lorem.sentence() }, departments], + params: [undefined, { name: faker.person.firstName(), description: faker.lorem.sentence() }, departments], id: '101', msg: 'method', }), @@ -27,7 +28,7 @@ export const saveTags = (departments: string[] = []): Promise => { export const removeTag = (id: string): Promise => { return new Promise((resolve, reject) => { - request + void request .post(methodCall(`livechat:removeTag`)) .set(credentials) .send({ @@ -43,7 +44,6 @@ export const removeTag = (id: string): Promise => { return reject(err); } resolve(JSON.parse(res.body.message).result); - } - ); + }); }); -}; \ No newline at end of file +}; diff --git a/apps/meteor/tests/data/livechat/triggers.ts b/apps/meteor/tests/data/livechat/triggers.ts index d3886cb34de79..4c8905a50825a 100644 --- a/apps/meteor/tests/data/livechat/triggers.ts +++ b/apps/meteor/tests/data/livechat/triggers.ts @@ -1,17 +1,36 @@ import { faker } from '@faker-js/faker'; import type { ILivechatTrigger } from '@rocket.chat/core-typings'; + import { api, credentials, methodCall, request } from '../api-data'; import type { DummyResponse } from './utils'; export const createTrigger = (name: string): Promise => { - return new Promise((resolve, reject) => { - request + return new Promise((resolve, reject) => { + void request .post(methodCall(`livechat:saveTrigger`)) .set(credentials) .send({ message: JSON.stringify({ method: 'livechat:saveTrigger', - params: [{ name, description: faker.lorem.sentence(), enabled: true, runOnce: faker.datatype.boolean(), actions: [{ name: 'send-message', params: { msg: faker.lorem.sentence(), name: faker.person.firstName(), sender: faker.helpers.arrayElement(['queue', 'custom']) } }], conditions: [{ name: faker.lorem.word(), value: faker.number.int() }] }], + params: [ + { + name, + description: faker.lorem.sentence(), + enabled: true, + runOnce: faker.datatype.boolean(), + actions: [ + { + name: 'send-message', + params: { + msg: faker.lorem.sentence(), + name: faker.person.firstName(), + sender: faker.helpers.arrayElement(['queue', 'custom']), + }, + }, + ], + conditions: [{ name: faker.lorem.word(), value: faker.number.int() }], + }, + ], id: '101', msg: 'method', }), @@ -26,15 +45,15 @@ export const createTrigger = (name: string): Promise => { }; export const fetchTriggers = (): Promise => { - return new Promise((resolve, reject) => { - request - .get(api('livechat/triggers')) - .set(credentials) - .end((err: Error, res: DummyResponse) => { - if (err) { - return reject(err); - } - resolve(res.body.triggers); - }); - }); + return new Promise((resolve, reject) => { + void request + .get(api('livechat/triggers')) + .set(credentials) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(res.body.triggers); + }); + }); }; diff --git a/apps/meteor/tests/data/livechat/units.ts b/apps/meteor/tests/data/livechat/units.ts index c42d7ad5b3458..03ea578e654da 100644 --- a/apps/meteor/tests/data/livechat/units.ts +++ b/apps/meteor/tests/data/livechat/units.ts @@ -1,11 +1,12 @@ -import { faker } from "@faker-js/faker"; -import type { IOmnichannelBusinessUnit } from "@rocket.chat/core-typings"; -import { methodCall, credentials, request } from "../api-data"; -import type { DummyResponse } from "./utils"; +import { faker } from '@faker-js/faker'; +import type { IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; + +import { methodCall, credentials, request } from '../api-data'; +import type { DummyResponse } from './utils'; export const createMonitor = async (username: string): Promise<{ _id: string; username: string }> => { - return new Promise((resolve, reject) => { - request + return new Promise((resolve, reject) => { + void request .post(methodCall(`livechat:addMonitor`)) .set(credentials) .send({ @@ -25,24 +26,34 @@ export const createMonitor = async (username: string): Promise<{ _id: string; us }); }; -export const createUnit = async (monitorId: string, username: string, departmentIds: string[], name?: string): Promise => { - return new Promise((resolve, reject) => { - request - .post(methodCall(`livechat:saveUnit`)) - .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:saveUnit', - params: [null, { name: name || faker.person.firstName(), visibility: faker.helpers.arrayElement(['public', 'private']) }, [{ monitorId, username }], departmentIds.map((departmentId) => ({ departmentId }))], - id: '101', - msg: 'method', - }), - }) - .end((err: Error, res: DummyResponse) => { - if (err) { - return reject(err); - } - resolve(JSON.parse(res.body.message).result); - }); - }); +export const createUnit = async ( + monitorId: string, + username: string, + departmentIds: string[], + name?: string, +): Promise => { + return new Promise((resolve, reject) => { + void request + .post(methodCall(`livechat:saveUnit`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveUnit', + params: [ + null, + { name: name || faker.person.firstName(), visibility: faker.helpers.arrayElement(['public', 'private']) }, + [{ monitorId, username }], + departmentIds.map((departmentId) => ({ departmentId })), + ], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); }; diff --git a/apps/meteor/tests/data/livechat/users.ts b/apps/meteor/tests/data/livechat/users.ts index 161c20749b6c6..9d0bfd04f170f 100644 --- a/apps/meteor/tests/data/livechat/users.ts +++ b/apps/meteor/tests/data/livechat/users.ts @@ -1,65 +1,66 @@ -import { faker } from "@faker-js/faker"; -import type { ILivechatAgent, IUser } from "@rocket.chat/core-typings"; -import { IUserCredentialsHeader, password } from "../user"; -import { createUser, login } from "../users.helper"; -import { createAgent, makeAgentAvailable, makeAgentUnavailable } from "./rooms"; -import { api, credentials, request } from "../api-data"; +import { faker } from '@faker-js/faker'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { ILivechatAgent, IUser } from '@rocket.chat/core-typings'; + +import { api, credentials, request } from '../api-data'; +import { password } from '../user'; +import { createUser, login } from '../users.helper'; +import { createAgent, makeAgentAvailable, makeAgentUnavailable } from './rooms'; export const createBotAgent = async (): Promise<{ - credentials: { 'X-Auth-Token': string; 'X-User-Id': string; }; + credentials: Credentials; user: IUser; }> => { - const agent: IUser = await createUser({ - roles: ['bot'] - }); + const agent = await createUser({ + roles: ['bot'], + }); const createdUserCredentials = await login(agent.username, password); await createAgent(agent.username); await makeAgentAvailable(createdUserCredentials); - return { - credentials: createdUserCredentials, - user: agent, - }; -} + return { + credentials: createdUserCredentials, + user: agent, + }; +}; export const getRandomVisitorToken = (): string => faker.string.alphanumeric(17); export const getAgent = async (userId: string): Promise => { - const { body } = await request.get(api(`livechat/users/agent/${userId}`)) - .set(credentials) - .expect(200); - return body.user; -} + const { body } = await request + .get(api(`livechat/users/agent/${userId}`)) + .set(credentials) + .expect(200); + return body.user; +}; export const removeAgent = async (userId: string): Promise => { - await request.delete(api(`livechat/users/agent/${userId}`)) - .set(credentials) - .expect(200); -} + await request + .delete(api(`livechat/users/agent/${userId}`)) + .set(credentials) + .expect(200); +}; export const createAnOnlineAgent = async (): Promise<{ - credentials: IUserCredentialsHeader; - user: IUser & { username: string }; + credentials: Credentials; + user: IUser & { username: string }; }> => { - const username = `user.test.${Date.now()}`; - const email = `${username}@rocket.chat`; - const { body } = await request - .post(api('users.create')) - .set(credentials) - .send({ email, name: username, username, password }); - const agent = body.user; - const createdUserCredentials = await login(agent.username, password); - await createAgent(agent.username); - await makeAgentAvailable(createdUserCredentials); + const username = `user.test.${Date.now()}`; + const email = `${username}@rocket.chat`; + const { body } = await request.post(api('users.create')).set(credentials).send({ email, name: username, username, password }); + const agent = body.user; + const createdUserCredentials = await login(agent.username, password); + await createAgent(agent.username); + await makeAgentAvailable(createdUserCredentials); - return { - credentials: createdUserCredentials, - user: agent, - }; -} + return { + credentials: createdUserCredentials, + user: agent, + }; +}; export const createAnOfflineAgent = async (): Promise<{ - credentials: IUserCredentialsHeader; + credentials: Credentials; user: IUser & { username: string }; }> => { const username = `user.test.${Date.now()}.offline`; @@ -74,4 +75,4 @@ export const createAnOfflineAgent = async (): Promise<{ credentials: createdUserCredentials, user: agent, }; -}; \ No newline at end of file +}; diff --git a/apps/meteor/tests/data/livechat/utils.ts b/apps/meteor/tests/data/livechat/utils.ts index 3d82ecdd58f74..cf4b906c18786 100644 --- a/apps/meteor/tests/data/livechat/utils.ts +++ b/apps/meteor/tests/data/livechat/utils.ts @@ -1,5 +1,4 @@ -export type DummyResponse = - E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T }; +export type DummyResponse = E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T }; export type WithRequiredProperty = Type & { [Property in Key]-?: Type[Property]; @@ -7,7 +6,7 @@ export type WithRequiredProperty = Type & { export const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); -} +}; export const parseMethodResponse = (response: any) => { if (response.message) { @@ -15,4 +14,4 @@ export const parseMethodResponse = (response: any) => { } return {}; -} \ No newline at end of file +}; diff --git a/apps/meteor/tests/data/livechat/visitor.ts b/apps/meteor/tests/data/livechat/visitor.ts index 86c1043fb05d0..ed29296bd27d6 100644 --- a/apps/meteor/tests/data/livechat/visitor.ts +++ b/apps/meteor/tests/data/livechat/visitor.ts @@ -1,9 +1,13 @@ -import { ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { ILivechatVisitor } from '@rocket.chat/core-typings'; import { expect } from 'chai'; + import { api, credentials, request } from '../api-data'; export const getLivechatVisitorByToken = async (token: string): Promise => { - const response = await request.get(api(`livechat/visitor/${token}`)).set(credentials).expect(200); - expect(response.body).to.have.property('visitor'); - return response.body.visitor; -} + const response = await request + .get(api(`livechat/visitor/${token}`)) + .set(credentials) + .expect(200); + expect(response.body).to.have.property('visitor'); + return response.body.visitor; +}; diff --git a/apps/meteor/tests/data/moderation.helper.ts b/apps/meteor/tests/data/moderation.helper.ts deleted file mode 100644 index 459fb43bad966..0000000000000 --- a/apps/meteor/tests/data/moderation.helper.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { api, credentials, request } from './api-data'; - -export const makeModerationApiRequest = async (url: string, method: 'get' | 'post', data?: any) => { - let res: any; - - if (method === 'get') { - res = await request.get(api(url)).set(credentials).query(data); - } else if (method === 'post') { - res = await request.post(api(url)).set(credentials).send(data); - } - - return res.body; -}; - -export const reportUser = (userId: string, reason: string) => makeModerationApiRequest('moderation.reportUser', 'post', { userId, reason }); - -export const getUsersReports = (userId: string) => makeModerationApiRequest('moderation.user.reportsByUserId', 'get', { userId }); diff --git a/apps/meteor/tests/data/permissions.helper.ts b/apps/meteor/tests/data/permissions.helper.ts index 4713176015115..33e9af1693adc 100644 --- a/apps/meteor/tests/data/permissions.helper.ts +++ b/apps/meteor/tests/data/permissions.helper.ts @@ -1,43 +1,44 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { IS_EE } from '../e2e/config/constants'; -import { api, credentials, request } from './api-data'; + import { permissions } from '../../app/authorization/server/constant/permissions'; import { omnichannelEEPermissions } from '../../ee/app/livechat-enterprise/server/permissions'; +import { IS_EE } from '../e2e/config/constants'; +import { api, credentials, request } from './api-data'; -export const updatePermission = (permission:string, roles:string[]):Promise => - new Promise((resolve,reject) => { - request +export const updatePermission = (permission: string, roles: string[]): Promise => + new Promise((resolve, reject) => { + void request .post(api('permissions.update')) .set(credentials) .send({ permissions: [{ _id: permission, roles }] }) .expect('Content-Type', 'application/json') .expect(200) - .end((err?:Error) => setTimeout(() => !err && resolve() || reject(err), 100)); + .end((err?: Error) => setTimeout(() => (!err && resolve()) || reject(err), 100)); }); -export const updateEEPermission = (permission:string, roles:string[]):Promise => +export const updateEEPermission = (permission: string, roles: string[]): Promise => IS_EE ? updatePermission(permission, roles) : Promise.resolve(); -export const updateManyPermissions = (permissions: { [key: string]: string[] }):Promise => - new Promise((resolve,reject) => { - request +const updateManyPermissions = (permissions: { [key: string]: string[] }): Promise => + new Promise((resolve, reject) => { + void request .post(api('permissions.update')) .set(credentials) - .send({ permissions: Object.keys(permissions).map((k) => ({_id: k, roles: permissions[k] }))}) + .send({ permissions: Object.keys(permissions).map((k) => ({ _id: k, roles: permissions[k] })) }) .expect('Content-Type', 'application/json') .expect(200) - .end((err?:Error) => setTimeout(() => !err && resolve() || reject(err), 100)); + .end((err?: Error) => setTimeout(() => (!err && resolve()) || reject(err), 100)); }); -export const updateSetting = (setting:string, value:ISetting['value']):Promise => - new Promise((resolve,reject) => { - request +export const updateSetting = (setting: string, value: ISetting['value']): Promise => + new Promise((resolve, reject) => { + void request .post(`/api/v1/settings/${setting}`) .set(credentials) .send({ value }) .expect('Content-Type', 'application/json') .expect(200) - .end((err?:Error) => setTimeout(() => !err && resolve() || reject(err), 100)); + .end((err?: Error) => setTimeout(() => (!err && resolve()) || reject(err), 100)); }); export const getSettingValueById = async (setting: string): Promise => { @@ -46,17 +47,18 @@ export const getSettingValueById = async (setting: string): Promise => - IS_EE ? new Promise((resolve,reject) => { - request - .post(`/api/v1/settings/${setting}`) - .set(credentials) - .send({ value }) - .expect('Content-Type', 'application/json') - .expect(200) - .end((err?:Error) => setTimeout(() => !err && resolve() || reject(err), 100)); - }) : Promise.resolve(); +export const updateEESetting = (setting: string, value: ISetting['value']): Promise => + IS_EE + ? new Promise((resolve, reject) => { + void request + .post(`/api/v1/settings/${setting}`) + .set(credentials) + .send({ value }) + .expect('Content-Type', 'application/json') + .expect(200) + .end((err?: Error) => setTimeout(() => (!err && resolve()) || reject(err), 100)); + }) + : Promise.resolve(); export const removePermissions = async (perms: string[]) => { await updateManyPermissions(Object.fromEntries(perms.map((name) => [name, []]))); @@ -66,37 +68,37 @@ export const addPermissions = async (perms: { [key: string]: string[] }) => { await updateManyPermissions(perms); }; -type Permission = typeof permissions[number]['_id'] +type Permission = (typeof permissions)[number]['_id']; export const removePermissionFromAllRoles = async (permission: Permission) => { - await updatePermission(permission, []); + await updatePermission(permission, []); }; -export const restorePermissionToRoles = async (permission: Permission) => { - const defaultPermission = getPermissions().find((p) => p._id === permission); - if (!defaultPermission) { - throw new Error(`No default roles found for permission ${permission}`); - } - - const mutableDefaultRoles: string[] = defaultPermission.roles.map((r) => r); +const getPermissions = () => { + if (!IS_EE) { + return permissions; + } - if (!IS_EE) { - const eeOnlyRoles = ['livechat-monitor']; - eeOnlyRoles.forEach((role) => { - const index = mutableDefaultRoles.indexOf(role); - if (index !== -1) { - mutableDefaultRoles.splice(index, 1); - } - }); - } + return [...permissions, ...omnichannelEEPermissions]; +}; - await updatePermission(permission, mutableDefaultRoles); -} +export const restorePermissionToRoles = async (permission: Permission) => { + const defaultPermission = getPermissions().find((p) => p._id === permission); + if (!defaultPermission) { + throw new Error(`No default roles found for permission ${permission}`); + } -const getPermissions = () => { - if (!IS_EE) { - return permissions; - } + const mutableDefaultRoles: string[] = defaultPermission.roles.map((r) => r); - return [...permissions, ...omnichannelEEPermissions] -} + if (!IS_EE) { + const eeOnlyRoles = ['livechat-monitor']; + eeOnlyRoles.forEach((role) => { + const index = mutableDefaultRoles.indexOf(role); + if (index !== -1) { + mutableDefaultRoles.splice(index, 1); + } + }); + } + + await updatePermission(permission, mutableDefaultRoles); +}; diff --git a/apps/meteor/tests/data/role.ts b/apps/meteor/tests/data/role.ts deleted file mode 100644 index 2d68825f3a2e8..0000000000000 --- a/apps/meteor/tests/data/role.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const roleNameUsers = `role-name-test-users-${Date.now()}`; -export const roleNameSubscriptions = `role-name-test-subscriptions-${Date.now()}`; -export const roleScopeUsers = 'Users'; -export const roleScopeSubscriptions = 'Subscriptions'; -export const roleDescription = `role-description-test-${Date.now()}`; diff --git a/apps/meteor/tests/data/rooms.helper.ts b/apps/meteor/tests/data/rooms.helper.ts index 384ac5614e41d..410e6e7ca48c6 100644 --- a/apps/meteor/tests/data/rooms.helper.ts +++ b/apps/meteor/tests/data/rooms.helper.ts @@ -1,8 +1,7 @@ +import type { Credentials } from '@rocket.chat/api-client'; import type { IRoom } from '@rocket.chat/core-typings'; -import { api, credentials, request } from './api-data'; -import type { IUser } from '@rocket.chat/core-typings'; -type Credentials = { 'X-Auth-Token'?: string; 'X-User-Id'?: string }; +import { api, credentials, request } from './api-data'; type CreateRoomParams = { name?: IRoom['name']; @@ -38,7 +37,8 @@ export const createRoom = ({ * is handled separately here. */ return request - .get(api(`voip/room?token=${token}&agentId=${agentId}&direction=${voipCallDirection}`)) + .get(api('voip/room')) + .query({ token, agentId, direction: voipCallDirection }) .set(customCredentials || credentials) .send(); } @@ -51,7 +51,7 @@ export const createRoom = ({ c: 'channels.create', p: 'groups.create', d: 'im.create', - }; + } as const; const params = type === 'd' ? { username } : { name }; // Safe assertion because we already checked the type is not 'v' @@ -68,13 +68,8 @@ export const createRoom = ({ }); }; -export const asyncCreateRoom = ({ name, type, username, members = [] }: Pick) => - new Promise((resolve) => { - createRoom({ name, type, username, members }).end(resolve); - }); - type ActionType = 'delete' | 'close' | 'addOwner' | 'removeOwner'; -type ActionRoomParams = { +export type ActionRoomParams = { action: ActionType; type: Exclude; roomId: IRoom['_id']; @@ -82,7 +77,7 @@ type ActionRoomParams = { extraData?: Record; }; -function actionRoom({ action, type, roomId, overrideCredentials = credentials, extraData = {} }: ActionRoomParams) { +export function actionRoom({ action, type, roomId, overrideCredentials = credentials, extraData = {} }: ActionRoomParams) { if (!type) { throw new Error(`"type" is required in "${action}Room" test helper`); } @@ -93,10 +88,15 @@ function actionRoom({ action, type, roomId, overrideCredentials = credentials, e c: 'channels', p: 'groups', d: 'im', - }; + } as const; + + const path = `${endpoints[type]}.${action}` as const; + + if (path === 'im.addOwner' || path === 'im.removeOwner') throw new Error(`invalid path ("${path}")`); + return new Promise((resolve) => { - request - .post(api(`${endpoints[type]}.${action}`)) + void request + .post(api(path)) .set(overrideCredentials) .send({ roomId, @@ -108,60 +108,3 @@ function actionRoom({ action, type, roomId, overrideCredentials = credentials, e export const deleteRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) => actionRoom({ action: 'delete', type, roomId, overrideCredentials: credentials }); - -export const closeRoom = ({ type, roomId }: { type: ActionRoomParams['type']; roomId: IRoom['_id'] }) => - actionRoom({ action: 'close', type, roomId }); - -export const joinChannel = ({ overrideCredentials = credentials, roomId }: { overrideCredentials: Credentials; roomId: IRoom['_id'] }) => - request.post(api('channels.join')).set(overrideCredentials).send({ - roomId, - }); - -export const inviteToChannel = ({ - overrideCredentials = credentials, - roomId, - userId, -}: { - overrideCredentials: Credentials; - roomId: IRoom['_id']; - userId: IUser['_id']; -}) => - request.post(api('channels.invite')).set(overrideCredentials).send({ - userId, - roomId, - }); - -export const addRoomOwner = ({ type, roomId, userId }: { type: ActionRoomParams['type']; roomId: IRoom['_id']; userId: IUser['_id'] }) => - actionRoom({ action: 'addOwner', type, roomId, extraData: { userId } }); - -export const removeRoomOwner = ({ type, roomId, userId }: { type: ActionRoomParams['type']; roomId: IRoom['_id']; userId: IUser['_id'] }) => - actionRoom({ action: 'removeOwner', type, roomId, extraData: { userId } }); - -export const getChannelRoles = async ({ - roomId, - overrideCredentials = credentials, -}: { - roomId: IRoom['_id']; - overrideCredentials: Credentials; -}) => - ( - await request.get(api('channels.roles')).set(overrideCredentials).query({ - roomId, - }) - ).body.roles; - -export const setRoomConfig = ({ roomId, favorite, isDefault }: { roomId: IRoom['_id']; favorite: boolean; isDefault: boolean }) => { - return request - .post(api('rooms.saveRoomSettings')) - .set(credentials) - .send({ - rid: roomId, - default: isDefault, - favorite: favorite - ? { - defaultValue: true, - favorite: false, - } - : undefined, - }); -}; diff --git a/apps/meteor/tests/data/subscriptions.ts b/apps/meteor/tests/data/subscriptions.ts deleted file mode 100644 index 04e0f48c98e4d..0000000000000 --- a/apps/meteor/tests/data/subscriptions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ISubscription } from "@rocket.chat/core-typings"; -import { api, credentials, request } from "./api-data"; - -export const getSubscriptionForRoom = async (roomId: string, overrideCredential?: { 'X-Auth-Token': string; 'X-User-Id': string; }): Promise => { - const response = await request - .get(api('subscriptions.getOne')) - .set(overrideCredential || credentials) - .query({ roomId }) - .expect('Content-Type', 'application/json') - .expect(200); - - const { subscription } = response.body; - - return subscription; -} diff --git a/apps/meteor/tests/data/teams.helper.ts b/apps/meteor/tests/data/teams.helper.ts index 62da06eea71a0..8fc60bd19fd43 100644 --- a/apps/meteor/tests/data/teams.helper.ts +++ b/apps/meteor/tests/data/teams.helper.ts @@ -1,28 +1,18 @@ -import { ITeam, TEAM_TYPE } from "@rocket.chat/core-typings" -import { api, request } from "./api-data" -import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import type { ITeam, TEAM_TYPE } from '@rocket.chat/core-typings'; + +import { api, request } from './api-data'; export const createTeam = async (credentials: Record, teamName: string, type: TEAM_TYPE): Promise => { - const response = await request.post(api('teams.create')).set(credentials).send({ - name: teamName, - type, - }); + const response = await request.post(api('teams.create')).set(credentials).send({ + name: teamName, + type, + }); - return response.body.team; + return response.body.team; }; export const deleteTeam = async (credentials: Record, teamName: string): Promise => { - await request.post(api('teams.delete')).set(credentials).send({ - teamName, - }); -}; - -export const addMembers = async (credentials: Record, teamName: string, members: IUser['id'][]): Promise => { - await request - .post(api('teams.addMembers')) - .set(credentials) - .send({ - teamName: teamName, - members: members.map((userId) => ({ userId, roles: ['member'] })) - }); + await request.post(api('teams.delete')).set(credentials).send({ + teamName, + }); }; diff --git a/apps/meteor/tests/data/uploads.helper.ts b/apps/meteor/tests/data/uploads.helper.ts index 5f6e4924d9ffc..ad95587527e35 100644 --- a/apps/meteor/tests/data/uploads.helper.ts +++ b/apps/meteor/tests/data/uploads.helper.ts @@ -1,15 +1,15 @@ -import type { Response } from 'supertest'; +import type { IRoom } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, it } from 'mocha'; +import type { Response } from 'supertest'; -import { api, request, credentials } from './api-data.js'; -import { password } from './user'; -import { createUser, login } from './users.helper'; +import { api, request, credentials } from './api-data'; import { imgURL } from './interactions'; +import { createVisitor } from './livechat/rooms'; import { updateSetting } from './permissions.helper'; import { createRoom, deleteRoom } from './rooms.helper'; -import { createVisitor } from './livechat/rooms'; -import { IRoom } from '@rocket.chat/core-typings'; +import { password } from './user'; +import { createUser, login } from './users.helper'; export async function testFileUploads( filesEndpoint: 'channels.files' | 'groups.files' | 'im.files', @@ -56,8 +56,8 @@ export async function testFileUploads( return roomResponse.body.room; }; - it('should fail if invalid channel', function (done) { - request + it('should fail if invalid channel', (done) => { + void request .get(api(filesEndpoint)) .set(credentials) .query({ @@ -65,16 +65,16 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(400) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', invalidRoomError); }) .end(done); }); - it('should fail for room type v', async function () { + it('should fail for room type v', async () => { const { _id } = await createVoipRoom(); - request + void request .get(api(filesEndpoint)) .set(credentials) .query({ @@ -82,14 +82,14 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(400) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'error-room-not-found'); }); }); - it('should succeed when searching by roomId', function (done) { - request + it('should succeed when searching by roomId', (done) => { + void request .get(api(filesEndpoint)) .set(credentials) .query({ @@ -97,15 +97,15 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('files').and.to.be.an('array'); }) .end(done); }); - it('should succeed when searching by roomId even requested with count and offset params', function (done) { - request + it('should succeed when searching by roomId even requested with count and offset params', (done) => { + void request .get(api(filesEndpoint)) .set(credentials) .query({ @@ -115,7 +115,7 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('files').and.to.be.an('array'); }) @@ -126,7 +126,7 @@ export async function testFileUploads( if (!testRoom.name) { this.skip(); } - request + void request .get(api(filesEndpoint)) .set(credentials) .query({ @@ -134,7 +134,7 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('files').and.to.be.an('array'); }) @@ -145,7 +145,7 @@ export async function testFileUploads( if (!testRoom.name) { this.skip(); } - request + void request .get(api(filesEndpoint)) .set(credentials) .query({ @@ -155,21 +155,21 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('files').and.to.be.an('array'); }) .end(done); }); - it('should not return thumbnails', async function () { + it('should not return thumbnails', async () => { await request .post(api(`rooms.upload/${testRoom._id}`)) .set(credentials) .attach('file', imgURL) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); }); @@ -181,19 +181,19 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('files').and.to.be.an('array').with.lengthOf(1); const { files } = res.body; - files.forEach(function (file: unknown) { + files.forEach((file: unknown) => { expect(file).to.not.have.property('originalFileId'); }); }); }); - it('should not return hidden files', async function () { + it('should not return hidden files', async () => { let msgId; let fileId: string; @@ -203,7 +203,7 @@ export async function testFileUploads( .attach('file', imgURL) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); msgId = res.body.message._id; @@ -228,12 +228,12 @@ export async function testFileUploads( }) .expect('Content-Type', 'application/json') .expect(200) - .expect(function (res: Response) { + .expect((res: Response) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('files').and.to.be.an('array').with.lengthOf(1); const { files } = res.body; - files.forEach(function (file: unknown) { + files.forEach((file: unknown) => { expect(file).to.have.property('_id').to.not.be.equal(fileId); }); }); diff --git a/apps/meteor/tests/data/user.ts b/apps/meteor/tests/data/user.ts index f9086bd351bca..e7bbf03f83a25 100644 --- a/apps/meteor/tests/data/user.ts +++ b/apps/meteor/tests/data/user.ts @@ -1,45 +1,12 @@ -import { IUser } from "@rocket.chat/core-typings"; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IUser } from '@rocket.chat/core-typings'; -export const username = 'user.test'; -export const email = `${username}@rocket.chat`; export const password = 'rocket.chat'; -export const reason = 'rocket.chat.reason'; export const adminUsername = 'rocketchat.internal.admin.test'; export const adminEmail = `${adminUsername}@rocket.chat`; export const adminPassword = adminUsername; -export const preferences = { - data: { - newRoomNotification: 'door', - newMessageNotification: 'chime', - muteFocusedConversations: true, - clockMode: 1, - useEmojis: true, - convertAsciiEmoji: true, - saveMobileBandwidth: true, - collapseMediaByDefault: false, - autoImageLoad: true, - emailNotificationMode: 'mentions', - unreadAlert: true, - notificationsSoundVolume: 100, - desktopNotifications: 'default', - pushNotifications: 'default', - enableAutoAway: true, - highlights: [], - desktopNotificationRequireInteraction: false, - hideUsernames: false, - hideRoles: false, - displayAvatars: true, - hideFlexTab: false, - sendOnEnter: 'normal', - idleTimeLimit: 3600, - notifyCalendarEvents: false, - enableMobileRinging: false, - }, -}; - -export type IUserCredentialsHeader = { 'X-Auth-Token': string; 'X-User-Id': string; }; export type IUserWithCredentials = { - user: IUser; - credentials: IUserCredentialsHeader; + user: IUser; + credentials: Credentials; }; diff --git a/apps/meteor/tests/data/users.helper.js b/apps/meteor/tests/data/users.helper.js deleted file mode 100644 index 5f4d932048865..0000000000000 --- a/apps/meteor/tests/data/users.helper.js +++ /dev/null @@ -1,121 +0,0 @@ -import { UserStatus } from '@rocket.chat/core-typings'; -import { api, credentials, request } from './api-data'; -import { password } from './user'; -import { MongoClient } from 'mongodb'; -import { URL_MONGODB } from '../e2e/config/constants'; - -export const createUser = (userData = {}) => - new Promise((resolve, reject) => { - const username = userData.username || `user.test.${Date.now()}.${Math.random()}`; - const email = userData.email || `${username}@rocket.chat`; - request - .post(api('users.create')) - .set(credentials) - .send({ email, name: username, username, password, ...userData }) - .end((err, res) => { - if (err) { - return reject(err); - } - resolve(res.body.user); - }); - }); - -export const login = (username, password) => - new Promise((resolve) => { - request - .post(api('login')) - .send({ - user: username, - password, - }) - .end((err, res) => { - const userCredentials = {}; - userCredentials['X-Auth-Token'] = res.body.data.authToken; - userCredentials['X-User-Id'] = res.body.data.userId; - resolve(userCredentials); - }); - }); - -export const deleteUser = async (user, extraData = {}) => - request - .post(api('users.delete')) - .set(credentials) - .send({ - userId: user._id, - ...extraData, - }); - -export const getUserByUsername = (username) => - new Promise((resolve) => { - request - .get(api(`users.info?username=${username}`)) - .set(credentials) - .end((err, res) => { - resolve(res.body.user); - }); - }); - -export const getUserStatus = (userId) => - new Promise((resolve) => { - request - .get(api(`users.getStatus?userId=${userId}`)) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .end((end, res) => { - resolve(res.body); - }); - }); - -export const getMe = (overrideCredential = credentials) => - new Promise((resolve) => { - request - .get(api('me')) - .set(overrideCredential) - .expect('Content-Type', 'application/json') - .expect(200) - .end((end, res) => { - resolve(res.body); - }); - }); - -export const setUserActiveStatus = (userId, activeStatus = true) => - new Promise((resolve) => { - request - .post(api('users.setActiveStatus')) - .set(credentials) - .send({ - userId, - activeStatus, - }) - .end(resolve); - }); - -export const setUserStatus = (overrideCredentials = credentials, status = UserStatus.ONLINE) => - request.post(api('users.setStatus')).set(overrideCredentials).send({ - message: '', - status, - }); - -export const registerUser = async (userData = {}, overrideCredentials = credentials) => { - const username = userData.username || `user.test.${Date.now()}`; - const email = userData.email || `${username}@rocket.chat`; - const result = await request - .post(api('users.register')) - .set(overrideCredentials) - .send({ email, name: username, username, pass: password, ...userData }); - - return result.body.user; -}; - -// For changing user data when it's not possible to do so via API -export const updateUserInDb = async (userId, userData) => { - const connection = await MongoClient.connect(URL_MONGODB); - - await connection - .db() - .collection('users') - .updateOne({ _id: userId }, { $set: { ...userData } }); - - await connection.close(); -}; diff --git a/apps/meteor/tests/data/users.helper.ts b/apps/meteor/tests/data/users.helper.ts new file mode 100644 index 0000000000000..ceeb671d3a4be --- /dev/null +++ b/apps/meteor/tests/data/users.helper.ts @@ -0,0 +1,102 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; + +import { api, credentials, request } from './api-data'; +import { password } from './user'; + +export type TestUser = TUser & { username: string; emails: string[] }; + +export const createUser = ( + userData: { + username?: string; + email?: string; + roles?: string[]; + active?: boolean; + joinDefaultChannels?: boolean; + verified?: boolean; + requirePasswordChange?: boolean; + name?: string; + password?: string; + } = {}, +) => + new Promise>((resolve, reject) => { + const username = userData.username || `user.test.${Date.now()}.${Math.random()}`; + const email = userData.email || `${username}@rocket.chat`; + void request + .post(api('users.create')) + .set(credentials) + .send({ email, name: username, username, password, ...userData }) + .end((err, res) => { + if (err) { + return reject(err); + } + resolve(res.body.user); + }); + }); + +export const login = (username: string | undefined, password: string): Promise => + new Promise((resolve) => { + void request + .post(api('login')) + .send({ + user: username, + password, + }) + .end((_err, res) => { + resolve({ + 'X-Auth-Token': res.body.data.authToken, + 'X-User-Id': res.body.data.userId, + }); + }); + }); + +export const deleteUser = async (user: Pick, extraData = {}) => + request + .post(api('users.delete')) + .set(credentials) + .send({ + userId: user._id, + ...extraData, + }); + +export const getUserByUsername = (username: string) => + new Promise>((resolve) => { + void request + .get(api('users.info')) + .query({ username }) + .set(credentials) + .end((_err, res) => { + resolve(res.body.user); + }); + }); + +export const getMe = (overrideCredential = credentials) => + new Promise>((resolve) => { + void request + .get(api('me')) + .set(overrideCredential) + .expect('Content-Type', 'application/json') + .expect(200) + .end((_end, res) => { + resolve(res.body); + }); + }); + +export const setUserActiveStatus = (userId: IUser['_id'], activeStatus = true) => + new Promise((resolve) => { + void request + .post(api('users.setActiveStatus')) + .set(credentials) + .send({ + userId, + activeStatus, + }) + .end(resolve); + }); + +export const setUserStatus = (overrideCredentials = credentials, status = UserStatus.ONLINE) => + request.post(api('users.setStatus')).set(overrideCredentials).send({ + message: '', + status, + }); diff --git a/apps/meteor/tests/e2e/config/constants.ts b/apps/meteor/tests/e2e/config/constants.ts index c938b693ff450..4c2a174e98aa2 100644 --- a/apps/meteor/tests/e2e/config/constants.ts +++ b/apps/meteor/tests/e2e/config/constants.ts @@ -6,7 +6,7 @@ export const BASE_API_URL = BASE_URL + API_PREFIX; export const IS_LOCALHOST = BASE_URL.startsWith('http://localhost'); -export const IS_EE = Boolean(process.env.IS_EE); +export const IS_EE = process.env.IS_EE ? !!JSON.parse(process.env.IS_EE) : false; export const URL_MONGODB = process.env.MONGO_URL || 'mongodb://localhost:3001/meteor?retryWrites=false'; diff --git a/apps/meteor/tests/end-to-end/api/00-autotranslate.js b/apps/meteor/tests/end-to-end/api/00-autotranslate.ts similarity index 88% rename from apps/meteor/tests/end-to-end/api/00-autotranslate.js rename to apps/meteor/tests/end-to-end/api/00-autotranslate.ts index 7397df9908494..3b5e03e8016f5 100644 --- a/apps/meteor/tests/end-to-end/api/00-autotranslate.js +++ b/apps/meteor/tests/end-to-end/api/00-autotranslate.ts @@ -1,12 +1,15 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, after, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { sendSimpleMessage } from '../../data/chat.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; -import { createUser, deleteUser, login } from '../../data/users.helper.js'; +import type { TestUser } from '../../data/users.helper'; +import { createUser, deleteUser, login } from '../../data/users.helper'; const resetAutoTranslateDefaults = async () => { await Promise.all([ @@ -21,9 +24,7 @@ const resetE2EDefaults = async () => { await Promise.all([updateSetting('E2E_Enabled_Default_PrivateRooms', false), updateSetting('E2E_Enable', false)]); }; -describe('AutoTranslate', function () { - this.retries(0); - +describe('AutoTranslate', () => { before((done) => getCredentials(done)); describe('[AutoTranslate]', () => { @@ -32,7 +33,7 @@ describe('AutoTranslate', function () { after(() => resetAutoTranslateDefaults()); it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', (done) => { - request + void request .get(api('autotranslate.getSupportedLanguages')) .set(credentials) .query({ @@ -47,9 +48,9 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the user does not have the "auto-translate" permission', (done) => { - updateSetting('AutoTranslate_Enabled', true).then(() => { - updatePermission('auto-translate', []).then(() => { - request + void updateSetting('AutoTranslate_Enabled', true).then(() => { + void updatePermission('auto-translate', []).then(() => { + void request .get(api('autotranslate.getSupportedLanguages')) .set(credentials) .query({ @@ -66,28 +67,27 @@ describe('AutoTranslate', function () { }); }); }); - it('should return a list of languages', (done) => { - updatePermission('auto-translate', ['admin']).then(() => { - request - .get(api('autotranslate.getSupportedLanguages')) - .set(credentials) - .query({ - targetLanguage: 'en', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.a.property('success', true); - expect(res.body.languages).to.be.an('array'); - }) - .end(done); - }); + + it('should return a list of languages', async () => { + await updatePermission('auto-translate', ['admin']); + await request + .get(api('autotranslate.getSupportedLanguages')) + .set(credentials) + .query({ + targetLanguage: 'en', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body.languages).to.be.an('array'); + }); }); }); describe('[/autotranslate.saveSettings', () => { - let testGroupId; - let testChannelId; + let testGroupId: IRoom['_id']; + let testChannelId: IRoom['_id']; before(async () => { await Promise.all([ @@ -110,7 +110,7 @@ describe('AutoTranslate', function () { }); it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -128,9 +128,9 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the user does not have the "auto-translate" permission', (done) => { - updateSetting('AutoTranslate_Enabled', true).then(() => { - updatePermission('auto-translate', []).then(() => { - request + void updateSetting('AutoTranslate_Enabled', true).then(() => { + void updatePermission('auto-translate', []).then(() => { + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -151,8 +151,8 @@ describe('AutoTranslate', function () { }); }); it('should throw an error when the bodyParam "roomId" is not provided', (done) => { - updatePermission('auto-translate', ['admin']).then(() => { - request + void updatePermission('auto-translate', ['admin']).then(() => { + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({}) @@ -165,7 +165,7 @@ describe('AutoTranslate', function () { }); }); it('should throw an error when the bodyParam "field" is not provided', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -179,7 +179,7 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the bodyParam "value" is not provided', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -194,7 +194,7 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the bodyParam "autoTranslate" is not a boolean', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -210,7 +210,7 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the bodyParam "autoTranslateLanguage" is not a string', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -226,7 +226,7 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the bodyParam "field" is invalid', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -242,7 +242,7 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the bodyParam "roomId" is invalid or the user is not subscribed', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -277,7 +277,7 @@ describe('AutoTranslate', function () { }); }); it('should return success when the setting is saved correctly', (done) => { - request + void request .post(api('autotranslate.saveSettings')) .set(credentials) .send({ @@ -295,8 +295,8 @@ describe('AutoTranslate', function () { }); describe('[/autotranslate.translateMessage', () => { - let messageSent; - let testChannelId; + let messageSent: IMessage; + let testChannelId: IRoom['_id']; before(async () => { await resetAutoTranslateDefaults(); @@ -314,7 +314,7 @@ describe('AutoTranslate', function () { }); it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', (done) => { - request + void request .post(api('autotranslate.translateMessage')) .set(credentials) .send({ @@ -330,9 +330,9 @@ describe('AutoTranslate', function () { .end(done); }); it('should throw an error when the bodyParam "messageId" is not provided', (done) => { - updateSetting('AutoTranslate_Enabled', true).then(() => { - updatePermission('auto-translate', ['admin']).then(() => { - request + void updateSetting('AutoTranslate_Enabled', true).then(() => { + void updatePermission('auto-translate', ['admin']).then(() => { + void request .post(api('autotranslate.translateMessage')) .set(credentials) .send({}) @@ -346,7 +346,7 @@ describe('AutoTranslate', function () { }); }); it('should throw an error when the bodyParam "messageId" is invalid', (done) => { - request + void request .post(api('autotranslate.translateMessage')) .set(credentials) .send({ @@ -361,7 +361,7 @@ describe('AutoTranslate', function () { .end(done); }); it('should return success when the translate is successful', (done) => { - request + void request .post(api('autotranslate.translateMessage')) .set(credentials) .send({ @@ -377,17 +377,17 @@ describe('AutoTranslate', function () { }); describe('Autoenable setting', () => { - let userA; - let userB; - let credA; - let credB; - let channel; - const channelsToRemove = []; - - const createChannel = async (members, cred) => + let userA: TestUser; + let userB: TestUser; + let credA: Credentials; + let credB: Credentials; + let channel: IRoom; + const channelsToRemove: IRoom[] = []; + + const createChannel = async (members: string[] | undefined, cred: Credentials) => (await createRoom({ type: 'c', members, name: `channel-test-${Date.now()}`, credentials: cred })).body.channel; - const setLanguagePref = async (language, cred) => { + const setLanguagePref = async (language: string, cred: Credentials) => { await request .post(api('users.setPreferences')) .set(cred) @@ -399,7 +399,7 @@ describe('AutoTranslate', function () { }); }; - const getSub = async (roomId, cred) => + const getSub = async (roomId: IRoom['_id'], cred: Credentials) => ( await request .get(api('subscriptions.getOne')) diff --git a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js b/apps/meteor/tests/end-to-end/api/00-miscellaneous.ts similarity index 92% rename from apps/meteor/tests/end-to-end/api/00-miscellaneous.js rename to apps/meteor/tests/end-to-end/api/00-miscellaneous.ts index 1fe04b9b380a6..6469ef051a245 100644 --- a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js +++ b/apps/meteor/tests/end-to-end/api/00-miscellaneous.ts @@ -1,26 +1,28 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IInstanceStatus, IRoom, ITeam, IUser } from '@rocket.chat/core-typings'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; -import { expect } from 'chai'; +import type { IInstance } from '@rocket.chat/rest-typings'; +import { AssertionError, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; import { adminEmail, adminUsername, adminPassword, password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login as doLogin } from '../../data/users.helper'; import { IS_EE } from '../../e2e/config/constants'; -describe('miscellaneous', function () { - this.retries(0); - +describe('miscellaneous', () => { before((done) => getCredentials(done)); describe('API default', () => { // Required by mobile apps describe('/info', () => { - let version; + let version: string; it('should return "version", "build", "commit" and "marketplaceApiVersion" when the user is logged in', (done) => { - request + void request .get('/api/info') .set(credentials) .expect('Content-Type', 'application/json') @@ -36,7 +38,7 @@ describe('miscellaneous', function () { .end(done); }); it('should return only "version" and the version should not have patch info when the user is not logged in', (done) => { - request + void request .get('/api/info') .expect('Content-Type', 'application/json') .expect(200) @@ -56,7 +58,7 @@ describe('miscellaneous', function () { }); it('/login (wrapper username)', (done) => { - request + void request .post(api('login')) .send({ user: { @@ -77,7 +79,7 @@ describe('miscellaneous', function () { }); it('/login (wrapper email)', (done) => { - request + void request .post(api('login')) .send({ user: { @@ -98,7 +100,7 @@ describe('miscellaneous', function () { }); it('/login by user', (done) => { - request + void request .post(api('login')) .send({ user: adminEmail, @@ -117,7 +119,7 @@ describe('miscellaneous', function () { }); it('/login by username', (done) => { - request + void request .post(api('login')) .send({ username: adminUsername, @@ -201,11 +203,11 @@ describe('miscellaneous', function () { }); describe('/directory', () => { - let user; - let testChannel; - let normalUserCredentials; - const teamName = `new-team-name-${Date.now()}`; - let teamCreated = {}; + let user: TestUser; + let testChannel: IRoom; + let normalUserCredentials: Credentials; + const teamName = `new-team-name-${Date.now()}` as const; + let teamCreated: ITeam; before(async () => { await updatePermission('create-team', ['admin', 'user']); @@ -225,7 +227,7 @@ describe('miscellaneous', function () { }); it('should return an array(result) when search by user and execute successfully', (done) => { - request + void request .get(api('directory')) .set(credentials) .query({ @@ -252,7 +254,7 @@ describe('miscellaneous', function () { }); it('should not return the emails field for non admins', (done) => { - request + void request .get(api('directory')) .set(normalUserCredentials) .query({ @@ -278,7 +280,7 @@ describe('miscellaneous', function () { .end(done); }); it('should return an array(result) when search by channel and execute successfully', (done) => { - request + void request .get(api('directory')) .set(credentials) .query({ @@ -303,7 +305,7 @@ describe('miscellaneous', function () { .end(done); }); it('should return an array(result) when search by channel with sort params correctly and execute successfully', (done) => { - request + void request .get(api('directory')) .set(credentials) .query({ @@ -331,7 +333,7 @@ describe('miscellaneous', function () { .end(done); }); it('should return an error when send invalid query', (done) => { - request + void request .get(api('directory')) .set(credentials) .query({ @@ -348,7 +350,7 @@ describe('miscellaneous', function () { .end(done); }); it('should return an error when have more than one sort parameter', (done) => { - request + void request .get(api('directory')) .set(credentials) .query({ @@ -370,7 +372,7 @@ describe('miscellaneous', function () { }); it('should return an object containing rooms and totalCount from teams', (done) => { - request + void request .get(api('directory')) .set(normalUserCredentials) .query({ @@ -405,11 +407,11 @@ describe('miscellaneous', function () { }); describe('[/spotlight]', () => { - let user; - let userCredentials; - let testChannel; - let testTeam; - let testChannelSpecialChars; + let user: TestUser; + let userCredentials: Credentials; + let testChannel: IRoom; + let testTeam: ITeam; + let testChannelSpecialChars: IRoom; const fnameSpecialCharsRoom = `test ГДΕληνικά-${Date.now()}`; const teamName = `team-test-${Date.now()}`; @@ -431,7 +433,7 @@ describe('miscellaneous', function () { }); it('should fail when does not have query param', (done) => { - request + void request .get(api('spotlight')) .set(credentials) .expect('Content-Type', 'application/json') @@ -443,7 +445,7 @@ describe('miscellaneous', function () { .end(done); }); it('should return object inside users array when search by a valid user', (done) => { - request + void request .get(api('spotlight')) .query({ query: `@${adminUsername}`, @@ -463,7 +465,7 @@ describe('miscellaneous', function () { .end(done); }); it('must return the object inside the room array when searching for a valid room and that user is not a member of it', (done) => { - request + void request .get(api('spotlight')) .query({ query: `#${testChannel.name}`, @@ -482,7 +484,7 @@ describe('miscellaneous', function () { .end(done); }); it('must return the teamMain property when searching for a valid team that the user is not a member of', (done) => { - request + void request .get(api('spotlight')) .query({ query: `${testTeam.name}`, @@ -502,7 +504,7 @@ describe('miscellaneous', function () { .end(done); }); it('must return rooms when searching for a valid fname', (done) => { - request + void request .get(api('spotlight')) .query({ query: `#${fnameSpecialCharsRoom}`, @@ -523,14 +525,14 @@ describe('miscellaneous', function () { }); describe('[/instances.get]', () => { - let unauthorizedUserCredentials; + let unauthorizedUserCredentials: Credentials; before(async () => { const createdUser = await createUser(); unauthorizedUserCredentials = await doLogin(createdUser.username, password); }); it('should fail if user is logged in but is unauthorized', (done) => { - request + void request .get(api('instances.get')) .set(unauthorizedUserCredentials) .expect('Content-Type', 'application/json') @@ -543,7 +545,7 @@ describe('miscellaneous', function () { }); it('should fail if not logged in', (done) => { - request + void request .get(api('instances.get')) .expect('Content-Type', 'application/json') .expect(401) @@ -555,7 +557,7 @@ describe('miscellaneous', function () { }); it('should return instances if user is logged in and is authorized', (done) => { - request + void request .get(api('instances.get')) .set(credentials) .expect(200) @@ -564,11 +566,15 @@ describe('miscellaneous', function () { expect(res.body).to.have.property('instances').and.to.be.an('array').with.lengthOf(1); - const { instances } = res.body; + const instances = res.body.instances as IInstance[]; const instanceName = IS_EE ? 'ddp-streamer' : 'rocket.chat'; - const instance = instances.filter((i) => i.instanceRecord.name === instanceName)[0]; + const instance = instances.find( + (i): i is IInstance & { instanceRecord: IInstanceStatus } => i.instanceRecord?.name === instanceName, + ); + + if (!instance) throw new AssertionError(`no instance named "${instanceName}"`); expect(instance).to.have.property('instanceRecord'); expect(instance).to.have.property('currentStatus'); @@ -604,7 +610,7 @@ describe('miscellaneous', function () { after(() => updateSetting('API_Enable_Shields', true)); it('should fail if API_Enable_Shields is disabled', (done) => { - request + void request .get(api('shield.svg')) .query({ type: 'online', @@ -622,8 +628,8 @@ describe('miscellaneous', function () { }); it('should succeed if API_Enable_Shields is enabled', (done) => { - updateSetting('API_Enable_Shields', true).then(() => { - request + void updateSetting('API_Enable_Shields', true).then(() => { + void request .get(api('shield.svg')) .query({ type: 'online', @@ -640,7 +646,7 @@ describe('miscellaneous', function () { describe('/pw.getPolicy', () => { it('should return policies', (done) => { - request + void request .get(api('pw.getPolicy')) .set(credentials) .expect('Content-Type', 'application/json') @@ -656,7 +662,7 @@ describe('miscellaneous', function () { describe('/pw.getPolicyReset', () => { it('should fail if no token provided', (done) => { - request + void request .get(api('pw.getPolicyReset')) .expect('Content-Type', 'application/json') .expect(400) @@ -668,8 +674,9 @@ describe('miscellaneous', function () { }); it('should fail if no token is invalid format', (done) => { - request - .get(api('pw.getPolicyReset?token=123')) + void request + .get(api('pw.getPolicyReset')) + .query({ token: '123' }) .expect('Content-Type', 'application/json') .expect(403) .expect((res) => { @@ -681,8 +688,9 @@ describe('miscellaneous', function () { // not sure we have a way to get the reset token, looks like it is only sent via email by Meteor it.skip('should return policies if correct token is provided', (done) => { - request - .get(api('pw.getPolicyReset?token')) + void request + .get(api('pw.getPolicyReset')) + .query({ token: '' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.ts similarity index 88% rename from apps/meteor/tests/end-to-end/api/01-users.js rename to apps/meteor/tests/end-to-end/api/01-users.ts index ee53820daa2a8..f34a424597af5 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.ts @@ -1,35 +1,186 @@ import crypto from 'crypto'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IGetRoomRoles, IRoom, ISubscription, ITeam, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; -import { expect } from 'chai'; +import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings'; +import { assert, expect } from 'chai'; import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; +import { MongoClient } from 'mongodb'; -import { getCredentials, api, request, credentials, apiEmail, apiUsername, log, wait, reservedWords } from '../../data/api-data.js'; -import { MAX_BIO_LENGTH, MAX_NICKNAME_LENGTH } from '../../data/constants.ts'; -import { customFieldText, clearCustomFields, setCustomFields } from '../../data/custom-fields.js'; +import { getCredentials, api, request, credentials, apiEmail, apiUsername, log, wait, reservedWords } from '../../data/api-data'; import { imgURL } from '../../data/interactions'; import { createAgent, makeAgentAvailable } from '../../data/livechat/rooms'; import { removeAgent, getAgent } from '../../data/livechat/users'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { - addRoomOwner, - createRoom, - deleteRoom, - getChannelRoles, - inviteToChannel, - joinChannel, - removeRoomOwner, - setRoomConfig, -} from '../../data/rooms.helper'; +import type { ActionRoomParams } from '../../data/rooms.helper'; +import { actionRoom, createRoom, deleteRoom } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; -import { adminEmail, preferences, password, adminUsername } from '../../data/user'; -import { createUser, login, deleteUser, getUserStatus, getUserByUsername, registerUser, updateUserInDb } from '../../data/users.helper.js'; +import type { IUserWithCredentials } from '../../data/user'; +import { adminEmail, password, adminUsername } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; +import { createUser, login, deleteUser, getUserByUsername } from '../../data/users.helper'; +import { IS_EE, URL_MONGODB } from '../../e2e/config/constants'; + +const MAX_BIO_LENGTH = 260; +const MAX_NICKNAME_LENGTH = 120; + +const customFieldText = { + type: 'text', + required: true, + minLength: 2, + maxLength: 10, +}; + +function setCustomFields(customFields: unknown) { + const stringified = customFields ? JSON.stringify(customFields) : ''; + + return request.post(api('settings/Accounts_CustomFields')).set(credentials).send({ value: stringified }).expect(200); +} + +function clearCustomFields() { + return setCustomFields(null); +} + +const joinChannel = ({ overrideCredentials = credentials, roomId }: { overrideCredentials: Credentials; roomId: IRoom['_id'] }) => + request.post(api('channels.join')).set(overrideCredentials).send({ + roomId, + }); + +const inviteToChannel = ({ + overrideCredentials = credentials, + roomId, + userId, +}: { + overrideCredentials?: Credentials; + roomId: IRoom['_id']; + userId: IUser['_id']; +}) => + request.post(api('channels.invite')).set(overrideCredentials).send({ + userId, + roomId, + }); -const targetUser = {}; +const addRoomOwner = ({ type, roomId, userId }: { type: ActionRoomParams['type']; roomId: IRoom['_id']; userId: IUser['_id'] }) => + actionRoom({ action: 'addOwner', type, roomId, extraData: { userId } }); + +const removeRoomOwner = ({ type, roomId, userId }: { type: ActionRoomParams['type']; roomId: IRoom['_id']; userId: IUser['_id'] }) => + actionRoom({ action: 'removeOwner', type, roomId, extraData: { userId } }); + +const getChannelRoles = async ({ + roomId, + overrideCredentials = credentials, +}: { + roomId: IRoom['_id']; + overrideCredentials?: Credentials; +}) => + ( + await request.get(api('channels.roles')).set(overrideCredentials).query({ + roomId, + }) + ).body.roles as IGetRoomRoles[]; + +const setRoomConfig = ({ roomId, favorite, isDefault }: { roomId: IRoom['_id']; favorite?: boolean; isDefault: boolean }) => { + return request + .post(api('rooms.saveRoomSettings')) + .set(credentials) + .send({ + rid: roomId, + default: isDefault, + favorite: favorite + ? { + defaultValue: true, + favorite: false, + } + : undefined, + }); +}; + +const preferences = { + data: { + newRoomNotification: 'door', + newMessageNotification: 'chime', + muteFocusedConversations: true, + clockMode: 1, + useEmojis: true, + convertAsciiEmoji: true, + saveMobileBandwidth: true, + collapseMediaByDefault: false, + autoImageLoad: true, + emailNotificationMode: 'mentions', + unreadAlert: true, + notificationsSoundVolume: 100, + desktopNotifications: 'default', + pushNotifications: 'default', + enableAutoAway: true, + highlights: [], + desktopNotificationRequireInteraction: false, + hideUsernames: false, + hideRoles: false, + displayAvatars: true, + hideFlexTab: false, + sendOnEnter: 'normal', + idleTimeLimit: 3600, + notifyCalendarEvents: false, + enableMobileRinging: false, + ...(IS_EE && { + omnichannelTranscriptPDF: false, + }), + }, +}; + +const getUserStatus = (userId: IUser['_id']) => + new Promise<{ + status: 'online' | 'offline' | 'away' | 'busy'; + message?: string; + _id?: string; + connectionStatus?: 'online' | 'offline' | 'away' | 'busy'; + }>((resolve) => { + void request + .get(api('users.getStatus')) + .query({ userId }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .end((_end, res) => { + resolve(res.body); + }); + }); -describe('[Users]', function () { - let userCredentials; - this.retries(0); +const registerUser = async ( + userData: { + username?: string; + email?: string; + name?: string; + pass?: string; + } = {}, + overrideCredentials = credentials, +) => { + const username = userData.username || `user.test.${Date.now()}`; + const email = userData.email || `${username}@rocket.chat`; + const result = await request + .post(api('users.register')) + .set(overrideCredentials) + .send({ email, name: username, username, pass: password, ...userData }); + + return result.body.user; +}; + +// For changing user data when it's not possible to do so via API +const updateUserInDb = async (userId: IUser['_id'], userData: Partial) => { + const connection = await MongoClient.connect(URL_MONGODB); + + await connection + .db() + .collection('users') + .updateOne({ _id: userId }, { $set: { ...userData } }); + + await connection.close(); +}; + +describe('[Users]', () => { + let targetUser: { _id: IUser['_id']; username: string }; + let userCredentials: Credentials; before((done) => getCredentials(done)); @@ -40,8 +191,10 @@ describe('[Users]', function () { joinDefaultChannels: true, verified: true, }); - targetUser._id = user._id; - targetUser.username = user.username; + targetUser = { + _id: user._id, + username: user.username, + }; userCredentials = await login(user.username, password); }); @@ -100,9 +253,7 @@ describe('[Users]', function () { const email = `customField_${apiEmail}`; const customFields = { customFieldText: 'success' }; - let user; - - await request + const res = await request .post(api('users.create')) .set(credentials) .send({ @@ -117,25 +268,24 @@ describe('[Users]', function () { customFields, }) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('user.username', username); - expect(res.body).to.have.nested.property('user.emails[0].address', email); - expect(res.body).to.have.nested.property('user.active', true); - expect(res.body).to.have.nested.property('user.name', username); - expect(res.body).to.have.nested.property('user.customFields.customFieldText', 'success'); - expect(res.body).to.not.have.nested.property('user.e2e'); + .expect(200); - user = res.body.user; - }); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.username', username); + expect(res.body).to.have.nested.property('user.emails[0].address', email); + expect(res.body).to.have.nested.property('user.active', true); + expect(res.body).to.have.nested.property('user.name', username); + expect(res.body).to.have.nested.property('user.customFields.customFieldText', 'success'); + expect(res.body).to.not.have.nested.property('user.e2e'); + + const { user } = res.body; await deleteUser(user); }); - function failCreateUser(name) { + function failCreateUser(name: string) { it(`should not create a new user if username is the reserved word ${name}`, (done) => { - request + void request .post(api('users.create')) .set(credentials) .send({ @@ -158,11 +308,11 @@ describe('[Users]', function () { }); } - function failUserWithCustomField(field) { + function failUserWithCustomField(field: { name: string; value: unknown; reason: string }) { it(`should not create a user if a custom field ${field.reason}`, async () => { await setCustomFields({ customFieldText }); - const customFields = {}; + const customFields: Record = {}; customFields[field.name] = field.value; await request @@ -201,7 +351,7 @@ describe('[Users]', function () { }); describe('users default roles configuration', () => { - const users = []; + const users: IUser[] = []; before(async () => { await updateSetting('Accounts_Registration_Users_Default_Roles', 'user,admin'); @@ -217,7 +367,7 @@ describe('[Users]', function () { const username = `defaultUserRole_${apiUsername}${Date.now()}`; const email = `defaultUserRole_${apiEmail}${Date.now()}`; - request + void request .post(api('users.create')) .set(credentials) .send({ @@ -245,7 +395,7 @@ describe('[Users]', function () { const username = `defaultUserRole_${apiUsername}${Date.now()}`; const email = `defaultUserRole_${apiEmail}${Date.now()}`; - request + void request .post(api('users.create')) .set(credentials) .send({ @@ -272,14 +422,14 @@ describe('[Users]', function () { }); describe('auto join default channels', () => { - let defaultTeamRoomId; - let defaultTeamId; - let group; - let user; - let userCredentials; - let user2; - let user3; - let userNoDefault; + let defaultTeamRoomId: IRoom['_id']; + let defaultTeamId: ITeam['_id']; + let group: IRoom; + let user: IUser; + let userCredentials: Credentials; + let user2: IUser; + let user3: IUser; + let userNoDefault: IUser; const teamName = `defaultTeam_${Date.now()}`; before(async () => { @@ -431,12 +581,12 @@ describe('[Users]', function () { describe('[/users.register]', () => { const email = `email@email${Date.now()}.com`; const username = `myusername${Date.now()}`; - let user; + let user: IUser; after(async () => deleteUser(user)); it('should register new user', (done) => { - request + void request .post(api('users.register')) .send({ email, @@ -456,7 +606,7 @@ describe('[Users]', function () { .end(done); }); it('should return an error when trying register new user with an existing username', (done) => { - request + void request .post(api('users.register')) .send({ email, @@ -475,7 +625,7 @@ describe('[Users]', function () { }); describe('[/users.info]', () => { - let infoRoom; + let infoRoom: IRoom; before(async () => { infoRoom = ( @@ -496,7 +646,7 @@ describe('[Users]', function () { ); it('should return an error when the user does not exist', (done) => { - request + void request .get(api('users.info')) .set(credentials) .query({ @@ -512,7 +662,7 @@ describe('[Users]', function () { }); it('should query information about a user by userId', (done) => { - request + void request .get(api('users.info')) .set(credentials) .query({ @@ -531,7 +681,7 @@ describe('[Users]', function () { }); it('should return "rooms" property when user request it and the user has the necessary permission (admin, "view-other-user-channels")', (done) => { - request + void request .get(api('users.info')) .set(credentials) .query({ @@ -543,14 +693,14 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('user.rooms').and.to.be.an('array'); - const createdRoom = res.body.user.rooms.find((room) => room.rid === infoRoom._id); + const createdRoom = (res.body.user.rooms as ISubscription[]).find((room) => room.rid === infoRoom._id); expect(createdRoom).to.have.property('unread'); }) .end(done); }); it('should NOT return "rooms" property when user NOT request it but the user has the necessary permission (admin, "view-other-user-channels")', (done) => { - request + void request .get(api('users.info')) .set(credentials) .query({ @@ -565,8 +715,8 @@ describe('[Users]', function () { .end(done); }); it('should return the rooms when the user request your own rooms but he does NOT have the necessary permission', (done) => { - updatePermission('view-other-user-channels', []).then(() => { - request + void updatePermission('view-other-user-channels', []).then(() => { + void request .get(api('users.info')) .set(credentials) .query({ @@ -585,8 +735,8 @@ describe('[Users]', function () { }); }); it("should NOT return the rooms when the user request another user's rooms and he does NOT have the necessary permission", (done) => { - updatePermission('view-other-user-channels', []).then(() => { - request + void updatePermission('view-other-user-channels', []).then(() => { + void request .get(api('users.info')) .set(credentials) .query({ @@ -603,8 +753,8 @@ describe('[Users]', function () { }); }); it("should NOT return any services fields when request to another user's info even if the user has the necessary permission", (done) => { - updatePermission('view-full-other-user-info', ['admin']).then(() => { - request + void updatePermission('view-full-other-user-info', ['admin']).then(() => { + void request .get(api('users.info')) .set(credentials) .query({ @@ -621,8 +771,8 @@ describe('[Users]', function () { }); }); it('should return all services fields when request for myself data even without privileged permission', (done) => { - updatePermission('view-full-other-user-info', []).then(() => { - request + void updatePermission('view-full-other-user-info', []).then(() => { + void request .get(api('users.info')) .set(credentials) .query({ @@ -670,7 +820,7 @@ describe('[Users]', function () { }); describe('[/users.getPresence]', () => { it("should query a user's presence by userId", (done) => { - request + void request .get(api('users.getPresence')) .set(credentials) .query({ @@ -686,8 +836,8 @@ describe('[Users]', function () { }); describe('Logging in with type: "resume"', () => { - let user; - let userCredentials; + let user: TestUser; + let userCredentials: Credentials; before(async () => { user = await createUser({ joinDefaultChannels: false }); @@ -724,7 +874,7 @@ describe('[Users]', function () { describe('[/users.presence]', () => { describe('Not logged in:', () => { it('should return 401 unauthorized', (done) => { - request + void request .get(api('users.presence')) .expect('Content-Type', 'application/json') .expect(401) @@ -736,7 +886,7 @@ describe('[Users]', function () { }); describe('Logged in:', () => { it('should return online users full list', (done) => { - request + void request .get(api('users.presence')) .set(credentials) .expect('Content-Type', 'application/json') @@ -745,7 +895,7 @@ describe('[Users]', function () { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('full', true); - const user = res.body.users.find((user) => user.username === 'rocket.cat'); + const user = (res.body.users as IUser[]).find((user) => user.username === 'rocket.cat'); expect(user).to.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset'); }) @@ -753,8 +903,9 @@ describe('[Users]', function () { }); it('should return no online users updated after now', (done) => { - request - .get(api(`users.presence?from=${new Date().toISOString()}`)) + void request + .get(api('users.presence')) + .query({ from: new Date().toISOString() }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -770,8 +921,9 @@ describe('[Users]', function () { const date = new Date(); date.setMinutes(date.getMinutes() - 11); - request - .get(api(`users.presence?from=${date.toISOString()}`)) + void request + .get(api('users.presence')) + .query({ from: date.toISOString() }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -779,7 +931,7 @@ describe('[Users]', function () { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('full', true); - const user = res.body.users.find((user) => user.username === 'rocket.cat'); + const user = (res.body.users as IUser[]).find((user) => user.username === 'rocket.cat'); expect(user).to.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset'); }) @@ -789,10 +941,10 @@ describe('[Users]', function () { }); describe('[/users.list]', () => { - let user; - let deactivatedUser; - let user2; - let user2Credentials; + let user: TestUser; + let deactivatedUser: TestUser; + let user2: TestUser; + let user2Credentials: Credentials; before(async () => { const username = `deactivated_${Date.now()}${apiUsername}`; @@ -863,7 +1015,7 @@ describe('[Users]', function () { ); it('should query all users in the system', (done) => { - request + void request .get(api('users.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -872,7 +1024,7 @@ describe('[Users]', function () { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('count'); expect(res.body).to.have.property('total'); - const myself = res.body.users.find((user) => user.username === adminUsername); + const myself = (res.body.users as IUser[]).find((user) => user.username === adminUsername); expect(myself).to.not.have.property('e2e'); }) .end(done); @@ -890,7 +1042,7 @@ describe('[Users]', function () { }), }; - request + void request .get(api('users.list')) .query(query) .set(credentials) @@ -901,7 +1053,8 @@ describe('[Users]', function () { expect(res.body).to.have.property('count'); expect(res.body).to.have.property('total'); expect(res.body).to.have.property('users'); - const queriedUser = res.body.users.find((u) => u._id === user._id); + const queriedUser = (res.body.users as IUser[]).find((u) => u._id === user._id); + assert.isDefined(queriedUser); expect(queriedUser).to.have.property('customFields'); expect(queriedUser.customFields).to.have.property('customFieldText', 'success'); }) @@ -921,7 +1074,7 @@ describe('[Users]', function () { }), }; - request + void request .get(api('users.list')) .query(query) .set(credentials) @@ -932,7 +1085,7 @@ describe('[Users]', function () { expect(res.body).to.have.property('count'); expect(res.body).to.have.property('total'); expect(res.body).to.have.property('users'); - const firstUser = res.body.users.find((u) => u._id === deactivatedUser._id); + const firstUser = (res.body.users as IUser[]).find((u) => u._id === deactivatedUser._id); expect(firstUser).to.have.property('active', false); }) .end(done); @@ -940,14 +1093,13 @@ describe('[Users]', function () { it.skip('should query all users in the system by name', (done) => { // filtering user list - request + void request .get(api('users.list')) .set(credentials) .query({ name: { $regex: 'g' }, }) .field('username', 1) - .sort('createdAt', -1) .expect(log) .expect('Content-Type', 'application/json') .expect(200) @@ -982,8 +1134,8 @@ describe('[Users]', function () { }); describe('Avatars', () => { - let user; - let userCredentials; + let user: TestUser; + let userCredentials: Credentials; before(async () => { user = await createUser(); @@ -1004,7 +1156,7 @@ describe('[Users]', function () { describe('[/users.setAvatar]', () => { it('should set the avatar of the logged user by a local image', (done) => { - request + void request .post(api('users.setAvatar')) .set(userCredentials) .attach('image', imgURL) @@ -1016,7 +1168,7 @@ describe('[Users]', function () { .end(done); }); it('should update the avatar of another user by userId when the logged user has the necessary permission (edit-other-user-avatar)', (done) => { - request + void request .post(api('users.setAvatar')) .set(userCredentials) .attach('image', imgURL) @@ -1029,7 +1181,7 @@ describe('[Users]', function () { .end(done); }); it('should set the avatar of another user by username and local image when the logged user has the necessary permission (edit-other-user-avatar)', (done) => { - request + void request .post(api('users.setAvatar')) .set(credentials) .attach('image', imgURL) @@ -1042,8 +1194,8 @@ describe('[Users]', function () { .end(done); }); it("should prevent from updating someone else's avatar when the logged user doesn't have the necessary permission(edit-other-user-avatar)", (done) => { - updatePermission('edit-other-user-avatar', []).then(() => { - request + void updatePermission('edit-other-user-avatar', []).then(() => { + void request .post(api('users.setAvatar')) .set(userCredentials) .attach('image', imgURL) @@ -1057,9 +1209,9 @@ describe('[Users]', function () { }); }); it('should allow users with the edit-other-user-avatar permission to update avatars when the Accounts_AllowUserAvatarChange setting is off', (done) => { - updateSetting('Accounts_AllowUserAvatarChange', false).then(() => { - updatePermission('edit-other-user-avatar', ['admin']).then(() => { - request + void updateSetting('Accounts_AllowUserAvatarChange', false).then(() => { + void updatePermission('edit-other-user-avatar', ['admin']).then(() => { + void request .post(api('users.setAvatar')) .set(credentials) .attach('image', imgURL) @@ -1084,7 +1236,7 @@ describe('[Users]', function () { }); it('should set the avatar of the logged user by a local image', (done) => { - request + void request .post(api('users.setAvatar')) .set(userCredentials) .attach('image', imgURL) @@ -1096,7 +1248,7 @@ describe('[Users]', function () { .end(done); }); it('should reset the avatar of the logged user', (done) => { - request + void request .post(api('users.resetAvatar')) .set(userCredentials) .expect('Content-Type', 'application/json') @@ -1110,7 +1262,7 @@ describe('[Users]', function () { .end(done); }); it('should reset the avatar of another user by userId when the logged user has the necessary permission (edit-other-user-avatar)', (done) => { - request + void request .post(api('users.resetAvatar')) .set(userCredentials) .send({ @@ -1124,7 +1276,7 @@ describe('[Users]', function () { .end(done); }); it('should reset the avatar of another user by username and local image when the logged user has the necessary permission (edit-other-user-avatar)', (done) => { - request + void request .post(api('users.resetAvatar')) .set(credentials) .send({ @@ -1138,8 +1290,8 @@ describe('[Users]', function () { .end(done); }); it("should prevent from resetting someone else's avatar when the logged user doesn't have the necessary permission(edit-other-user-avatar)", (done) => { - updatePermission('edit-other-user-avatar', []).then(() => { - request + void updatePermission('edit-other-user-avatar', []).then(() => { + void request .post(api('users.resetAvatar')) .set(userCredentials) .send({ @@ -1154,9 +1306,9 @@ describe('[Users]', function () { }); }); it('should allow users with the edit-other-user-avatar permission to reset avatars when the Accounts_AllowUserAvatarChange setting is off', (done) => { - updateSetting('Accounts_AllowUserAvatarChange', false).then(() => { - updatePermission('edit-other-user-avatar', ['admin']).then(() => { - request + void updateSetting('Accounts_AllowUserAvatarChange', false).then(() => { + void updatePermission('edit-other-user-avatar', ['admin']).then(() => { + void request .post(api('users.resetAvatar')) .set(credentials) .send({ @@ -1175,7 +1327,7 @@ describe('[Users]', function () { describe('[/users.getAvatar]', () => { it('should get the url of the avatar of the logged user via userId', (done) => { - request + void request .get(api('users.getAvatar')) .set(userCredentials) .query({ @@ -1185,7 +1337,7 @@ describe('[Users]', function () { .end(done); }); it('should get the url of the avatar of the logged user via username', (done) => { - request + void request .get(api('users.getAvatar')) .set(userCredentials) .query({ @@ -1198,11 +1350,11 @@ describe('[Users]', function () { describe('[/users.getAvatarSuggestion]', () => { it('should return 401 unauthorized when user is not logged in', (done) => { - request.get(api('users.getAvatarSuggestion')).expect('Content-Type', 'application/json').expect(401).end(done); + void request.get(api('users.getAvatarSuggestion')).expect('Content-Type', 'application/json').expect(401).end(done); }); it('should get avatar suggestion of the logged user via userId', (done) => { - request + void request .get(api('users.getAvatarSuggestion')) .set(userCredentials) .query({ @@ -1243,7 +1395,7 @@ describe('[Users]', function () { ); it("should update a user's info by userId", (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1271,7 +1423,7 @@ describe('[Users]', function () { }); it("should update a user's email by userId", (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1292,7 +1444,7 @@ describe('[Users]', function () { }); it("should update a user's bio by userId", (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1312,7 +1464,7 @@ describe('[Users]', function () { }); it("should update a user's nickname by userId", (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1332,7 +1484,7 @@ describe('[Users]', function () { }); it(`should return an error when trying to set a nickname longer than ${MAX_NICKNAME_LENGTH} characters`, (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1354,7 +1506,7 @@ describe('[Users]', function () { }); it(`should return an error when trying to set a bio longer than ${MAX_BIO_LENGTH} characters`, (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1373,7 +1525,7 @@ describe('[Users]', function () { }); it("should update a bot's email", (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1389,7 +1541,7 @@ describe('[Users]', function () { }); it("should verify user's email by userId", (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1409,9 +1561,9 @@ describe('[Users]', function () { }); it('should return an error when trying update username and it is not allowed', (done) => { - updatePermission('edit-other-user-info', ['user']).then(() => { - updateSetting('Accounts_AllowUsernameChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['user']).then(() => { + void updateSetting('Accounts_AllowUsernameChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1450,9 +1602,9 @@ describe('[Users]', function () { }); it('should return an error when trying update user real name and it is not allowed', (done) => { - updatePermission('edit-other-user-info', ['user']).then(() => { - updateSetting('Accounts_AllowRealNameChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['user']).then(() => { + void updateSetting('Accounts_AllowRealNameChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1472,9 +1624,9 @@ describe('[Users]', function () { }); it('should update user real name when the required permission is applied', (done) => { - updatePermission('edit-other-user-info', ['admin']).then(() => { - updateSetting('Accounts_AllowRealNameChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['admin']).then(() => { + void updateSetting('Accounts_AllowRealNameChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1494,9 +1646,9 @@ describe('[Users]', function () { }); it('should return an error when trying update user status message and it is not allowed', (done) => { - updatePermission('edit-other-user-info', ['user']).then(() => { - updateSetting('Accounts_AllowUserStatusMessageChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['user']).then(() => { + void updateSetting('Accounts_AllowUserStatusMessageChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1516,9 +1668,9 @@ describe('[Users]', function () { }); it('should update user status message when the required permission is applied', (done) => { - updatePermission('edit-other-user-info', ['admin']).then(() => { - updateSetting('Accounts_AllowUserStatusMessageChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['admin']).then(() => { + void updateSetting('Accounts_AllowUserStatusMessageChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1538,9 +1690,9 @@ describe('[Users]', function () { }); it('should return an error when trying update user email and it is not allowed', (done) => { - updatePermission('edit-other-user-info', ['user']).then(() => { - updateSetting('Accounts_AllowEmailChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['user']).then(() => { + void updateSetting('Accounts_AllowEmailChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1560,9 +1712,9 @@ describe('[Users]', function () { }); it('should update user email when the required permission is applied', (done) => { - updatePermission('edit-other-user-info', ['admin']).then(() => { - updateSetting('Accounts_AllowEmailChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['admin']).then(() => { + void updateSetting('Accounts_AllowEmailChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1582,9 +1734,9 @@ describe('[Users]', function () { }); it('should return an error when trying update user password and it is not allowed', (done) => { - updatePermission('edit-other-user-password', ['user']).then(() => { - updateSetting('Accounts_AllowPasswordChange', false).then(() => { - request + void updatePermission('edit-other-user-password', ['user']).then(() => { + void updateSetting('Accounts_AllowPasswordChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1604,9 +1756,9 @@ describe('[Users]', function () { }); it('should update user password when the required permission is applied', (done) => { - updatePermission('edit-other-user-password', ['admin']).then(() => { - updateSetting('Accounts_AllowPasswordChange', false).then(() => { - request + void updatePermission('edit-other-user-password', ['admin']).then(() => { + void updateSetting('Accounts_AllowPasswordChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1626,9 +1778,9 @@ describe('[Users]', function () { }); it('should return an error when trying update profile and it is not allowed', (done) => { - updatePermission('edit-other-user-info', ['user']).then(() => { - updateSetting('Accounts_AllowUserProfileChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['user']).then(() => { + void updateSetting('Accounts_AllowUserProfileChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1648,9 +1800,9 @@ describe('[Users]', function () { }); it('should update profile when the required permission is applied', (done) => { - updatePermission('edit-other-user-info', ['admin']).then(() => { - updateSetting('Accounts_AllowUserProfileChange', false).then(() => { - request + void updatePermission('edit-other-user-info', ['admin']).then(() => { + void updateSetting('Accounts_AllowUserProfileChange', false).then(() => { + void request .post(api('users.update')) .set(credentials) .send({ @@ -1697,9 +1849,9 @@ describe('[Users]', function () { await deleteUser(user); }); - function failUpdateUser(name) { + function failUpdateUser(name: string) { it(`should not update an user if the new username is the reserved word ${name}`, (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -1724,8 +1876,8 @@ describe('[Users]', function () { }); describe('[/users.updateOwnBasicInfo]', () => { - let user; - let userCredentials; + let user: TestUser; + let userCredentials: Credentials; before(async () => { user = await createUser(); @@ -1751,8 +1903,8 @@ describe('[Users]', function () { const editedEmail = `test${+new Date()}@mail.com`; it('enabling E2E in server and generating keys to user...', (done) => { - updateSetting('E2E_Enable', true).then(() => { - request + void updateSetting('E2E_Enable', true).then(() => { + void request .post(api('e2e.setUserPublicAndPrivateKeys')) .set(userCredentials) .send({ @@ -1769,7 +1921,7 @@ describe('[Users]', function () { }); it('should update the user own basic information', (done) => { - request + void request .post(api('users.updateOwnBasicInfo')) .set(userCredentials) .send({ @@ -1793,7 +1945,7 @@ describe('[Users]', function () { }); it('should update the user name only', (done) => { - request + void request .post(api('users.updateOwnBasicInfo')) .set(userCredentials) .send({ @@ -1813,7 +1965,7 @@ describe('[Users]', function () { }); it('should throw an error when user try change email without the password', (done) => { - request + void request .post(api('users.updateOwnBasicInfo')) .set(userCredentials) .send({ @@ -1827,7 +1979,7 @@ describe('[Users]', function () { }); it('should throw an error when user try change password without the actual password', (done) => { - request + void request .post(api('users.updateOwnBasicInfo')) .set(credentials) .send({ @@ -1841,7 +1993,7 @@ describe('[Users]', function () { }); it('should throw an error when the name is only whitespaces', (done) => { - request + void request .post(api('users.updateOwnBasicInfo')) .set(credentials) .send({ @@ -1858,7 +2010,7 @@ describe('[Users]', function () { }); it("should set new email as 'unverified'", (done) => { - request + void request .post(api('users.updateOwnBasicInfo')) .set(userCredentials) .send({ @@ -1879,9 +2031,9 @@ describe('[Users]', function () { .end(done); }); - function failUpdateUserOwnBasicInfo(name) { + function failUpdateUserOwnBasicInfo(name: string) { it(`should not update an user's basic info if the new username is the reserved word ${name}`, (done) => { - request + void request .post(api('users.updateOwnBasicInfo')) .set(credentials) .send({ @@ -2215,8 +2367,8 @@ describe('[Users]', function () { ...preferences.data, }, }; - updatePermission('edit-other-user-info', []).then(() => { - request + void updatePermission('edit-other-user-info', []).then(() => { + void request .post(api('users.setPreferences')) .set(credentials) .send(userPreferences) @@ -2237,8 +2389,8 @@ describe('[Users]', function () { ...preferences.data, }, }; - updatePermission('edit-other-user-info', ['admin', 'user']).then(() => { - request + void updatePermission('edit-other-user-info', ['admin', 'user']).then(() => { + void request .post(api('users.setPreferences')) .set(credentials) .send(userPreferences) @@ -2262,8 +2414,8 @@ describe('[Users]', function () { ...preferences.data, }, }; - updatePermission('edit-other-user-info', ['admin', 'user']).then(() => { - request + void updatePermission('edit-other-user-info', ['admin', 'user']).then(() => { + void request .post(api('users.setPreferences')) .set(credentials) .send(userPreferences) @@ -2285,7 +2437,7 @@ describe('[Users]', function () { ...preferences.data, }, }; - request + void request .post(api('users.setPreferences')) .set(credentials) .send(userPreferences) @@ -2306,7 +2458,7 @@ describe('[Users]', function () { language: 'en', }, }; - request + void request .post(api('users.setPreferences')) .set(credentials) .send(userPreferences) @@ -2326,7 +2478,7 @@ describe('[Users]', function () { ...preferences.data, language: 'en', }; - request + void request .get(api('users.getPreferences')) .set(credentials) .expect(200) @@ -2341,7 +2493,7 @@ describe('[Users]', function () { describe('[/users.forgotPassword]', () => { it('should send email to user (return success), when is a valid email', (done) => { - request + void request .post(api('users.forgotPassword')) .send({ email: adminEmail, @@ -2355,7 +2507,7 @@ describe('[Users]', function () { }); it('should not send email to user(return error), when is a invalid email', (done) => { - request + void request .post(api('users.forgotPassword')) .send({ email: 'invalidEmail', @@ -2371,7 +2523,7 @@ describe('[Users]', function () { describe('[/users.sendConfirmationEmail]', () => { it('should send email to user (return success), when is a valid email', (done) => { - request + void request .post(api('users.sendConfirmationEmail')) .set(credentials) .send({ @@ -2386,7 +2538,7 @@ describe('[Users]', function () { }); it('should not send email to user(return error), when is a invalid email', (done) => { - request + void request .post(api('users.sendConfirmationEmail')) .set(credentials) .send({ @@ -2403,8 +2555,8 @@ describe('[Users]', function () { describe('[/users.getUsernameSuggestion]', () => { const testUsername = `test${+new Date()}`; - let targetUser; - let userCredentials; + let targetUser: TestUser; + let userCredentials: Credentials; before(async () => { targetUser = await registerUser({ @@ -2419,7 +2571,7 @@ describe('[Users]', function () { after(() => deleteUser(targetUser)); it('should return an username suggestion', (done) => { - request + void request .get(api('users.getUsernameSuggestion')) .set(userCredentials) .expect('Content-Type', 'application/json') @@ -2433,8 +2585,8 @@ describe('[Users]', function () { }); describe('[/users.checkUsernameAvailability]', () => { - let targetUser; - let userCredentials; + let targetUser: TestUser; + let userCredentials: Credentials; before(async () => { targetUser = await registerUser(); @@ -2444,7 +2596,7 @@ describe('[Users]', function () { after(() => deleteUser(targetUser)); it('should return 401 unauthorized when user is not logged in', (done) => { - request + void request .get(api('users.checkUsernameAvailability')) .expect('Content-Type', 'application/json') .expect(401) @@ -2455,7 +2607,7 @@ describe('[Users]', function () { }); it('should return true if the username is the same user username set', (done) => { - request + void request .get(api('users.checkUsernameAvailability')) .set(userCredentials) .query({ @@ -2471,7 +2623,7 @@ describe('[Users]', function () { }); it('should return true if the username is available', (done) => { - request + void request .get(api('users.checkUsernameAvailability')) .set(userCredentials) .query({ @@ -2487,10 +2639,10 @@ describe('[Users]', function () { }); it('should return an error when the username is invalid', (done) => { - request + void request .get(api('users.checkUsernameAvailability')) .set(userCredentials) - .query() + .query({}) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { @@ -2501,8 +2653,8 @@ describe('[Users]', function () { }); describe('[/users.deleteOwnAccount]', () => { - let targetUser; - let userCredentials; + let targetUser: TestUser; + let userCredentials: Credentials; before(async () => { targetUser = await registerUser(); @@ -2512,7 +2664,7 @@ describe('[Users]', function () { after(async () => deleteUser(targetUser)); it('Enable "Accounts_AllowDeleteOwnAccount" setting...', (done) => { - request + void request .post('/api/v1/settings/Accounts_AllowDeleteOwnAccount') .set(credentials) .send({ value: true }) @@ -2525,7 +2677,7 @@ describe('[Users]', function () { }); it('should delete user own account', (done) => { - request + void request .post(api('users.deleteOwnAccount')) .set(userCredentials) .send({ @@ -2558,9 +2710,9 @@ describe('[Users]', function () { }); describe('last owner cases', () => { - let user; - let createdUserCredentials; - let room; + let user: TestUser; + let createdUserCredentials: Credentials; + let room: IRoom; beforeEach(async () => { user = await createUser(); @@ -2637,7 +2789,7 @@ describe('[Users]', function () { }); describe('[/users.delete]', () => { - let newUser; + let newUser: TestUser; before(async () => { newUser = await createUser(); @@ -2680,8 +2832,8 @@ describe('[Users]', function () { }); describe('last owner cases', () => { - let targetUser; - let room; + let targetUser: TestUser; + let room: IRoom; beforeEach(async () => { targetUser = await registerUser(); room = ( @@ -2763,7 +2915,7 @@ describe('[Users]', function () { describe('[/users.getPersonalAccessTokens]', () => { it('should return an array when the user does not have personal tokens configured', (done) => { - request + void request .get(api('users.getPersonalAccessTokens')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2778,7 +2930,7 @@ describe('[Users]', function () { describe('[/users.generatePersonalAccessToken]', () => { it('should return a personal access token to user', (done) => { - request + void request .post(api('users.generatePersonalAccessToken')) .set(credentials) .send({ @@ -2793,7 +2945,7 @@ describe('[Users]', function () { .end(done); }); it('should throw an error when user tries generate a token with the same name', (done) => { - request + void request .post(api('users.generatePersonalAccessToken')) .set(credentials) .send({ @@ -2809,7 +2961,7 @@ describe('[Users]', function () { }); describe('[/users.regeneratePersonalAccessToken]', () => { it('should return a personal access token to user when user regenerates the token', (done) => { - request + void request .post(api('users.regeneratePersonalAccessToken')) .set(credentials) .send({ @@ -2824,7 +2976,7 @@ describe('[Users]', function () { .end(done); }); it('should throw an error when user tries regenerate a token that does not exist', (done) => { - request + void request .post(api('users.regeneratePersonalAccessToken')) .set(credentials) .send({ @@ -2840,7 +2992,7 @@ describe('[Users]', function () { }); describe('[/users.getPersonalAccessTokens]', () => { it('should return my personal access tokens', (done) => { - request + void request .get(api('users.getPersonalAccessTokens')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2854,7 +3006,7 @@ describe('[Users]', function () { }); describe('[/users.removePersonalAccessToken]', () => { it('should return success when user remove a personal access token', (done) => { - request + void request .post(api('users.removePersonalAccessToken')) .set(credentials) .send({ @@ -2868,7 +3020,7 @@ describe('[Users]', function () { .end(done); }); it('should throw an error when user tries remove a token that does not exist', (done) => { - request + void request .post(api('users.removePersonalAccessToken')) .set(credentials) .send({ @@ -2889,7 +3041,7 @@ describe('[Users]', function () { describe('should return an error when the user dont have the necessary permission "create-personal-access-tokens"', () => { it('/users.generatePersonalAccessToken', (done) => { - request + void request .post(api('users.generatePersonalAccessToken')) .set(credentials) .send({ @@ -2904,7 +3056,7 @@ describe('[Users]', function () { .end(done); }); it('/users.regeneratePersonalAccessToken', (done) => { - request + void request .post(api('users.regeneratePersonalAccessToken')) .set(credentials) .send({ @@ -2919,7 +3071,7 @@ describe('[Users]', function () { .end(done); }); it('/users.getPersonalAccessTokens', (done) => { - request + void request .get(api('users.getPersonalAccessTokens')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2931,7 +3083,7 @@ describe('[Users]', function () { .end(done); }); it('/users.removePersonalAccessToken', (done) => { - request + void request .post(api('users.removePersonalAccessToken')) .set(credentials) .send({ @@ -2946,7 +3098,7 @@ describe('[Users]', function () { .end(done); }); it('should throw an error when user tries remove a token that does not exist', (done) => { - request + void request .post(api('users.removePersonalAccessToken')) .set(credentials) .send({ @@ -2965,10 +3117,10 @@ describe('[Users]', function () { }); describe('[/users.setActiveStatus]', () => { - let user; - let agent; - let agentUser; - let userCredentials; + let user: TestUser; + let agent: IUserWithCredentials; + let agentUser: TestUser; + let userCredentials: Credentials; before(async () => { agentUser = await createUser(); @@ -3002,7 +3154,7 @@ describe('[Users]', function () { after(() => Promise.all([removeAgent(agent.user._id), deleteUser(agent.user)])); it('should set other user active status to false when the logged user has the necessary permission(edit-other-user-active-status)', (done) => { - request + void request .post(api('users.setActiveStatus')) .set(userCredentials) .send({ @@ -3018,7 +3170,7 @@ describe('[Users]', function () { .end(done); }); it('should set other user active status to true when the logged user has the necessary permission(edit-other-user-active-status)', (done) => { - request + void request .post(api('users.setActiveStatus')) .set(userCredentials) .send({ @@ -3035,8 +3187,8 @@ describe('[Users]', function () { }); it('should return an error when trying to set other user active status and has not the necessary permission(edit-other-user-active-status)', (done) => { - updatePermission('edit-other-user-active-status', []).then(() => { - request + void updatePermission('edit-other-user-active-status', []).then(() => { + void request .post(api('users.setActiveStatus')) .set(userCredentials) .send({ @@ -3052,7 +3204,7 @@ describe('[Users]', function () { }); }); it('should return an error when trying to set user own active status and has not the necessary permission(edit-other-user-active-status)', (done) => { - request + void request .post(api('users.setActiveStatus')) .set(userCredentials) .send({ @@ -3067,8 +3219,8 @@ describe('[Users]', function () { .end(done); }); it('should set user own active status to false when the user has the necessary permission(edit-other-user-active-status)', (done) => { - updatePermission('edit-other-user-active-status', ['admin']).then(() => { - request + void updatePermission('edit-other-user-active-status', ['admin']).then(() => { + void request .post(api('users.setActiveStatus')) .set(userCredentials) .send({ @@ -3139,7 +3291,7 @@ describe('[Users]', function () { }); describe('last owner cases', () => { - let room; + let room: IRoom; beforeEach(() => Promise.all([ @@ -3252,7 +3404,7 @@ describe('[Users]', function () { expect(roles).to.have.lengthOf(2); const originalCreator = roles.find((role) => role.u._id === credentials['X-User-Id']); - expect(originalCreator).to.not.be.undefined; + assert.isDefined(originalCreator); expect(originalCreator.roles).to.eql(['owner']); expect(originalCreator.u).to.have.property('_id', credentials['X-User-Id']); }); @@ -3260,7 +3412,7 @@ describe('[Users]', function () { }); describe('[/users.deactivateIdle]', () => { - let testUser; + let testUser: TestUser; const testRoleId = 'guest'; before('Create test user', async () => { @@ -3282,8 +3434,8 @@ describe('[Users]', function () { after(() => Promise.all([deleteUser(testUser), updatePermission('edit-other-user-active-status', ['admin'])])); it('should fail to deactivate if user doesnt have edit-other-user-active-status permission', (done) => { - updatePermission('edit-other-user-active-status', []).then(() => { - request + void updatePermission('edit-other-user-active-status', []).then(() => { + void request .post(api('users.deactivateIdle')) .set(credentials) .send({ @@ -3299,8 +3451,8 @@ describe('[Users]', function () { }); }); it('should deactivate no users when no users in time range', (done) => { - updatePermission('edit-other-user-active-status', ['admin']).then(() => { - request + void updatePermission('edit-other-user-active-status', ['admin']).then(() => { + void request .post(api('users.deactivateIdle')) .set(credentials) .send({ @@ -3316,8 +3468,8 @@ describe('[Users]', function () { }); }); it('should deactivate the test user when given its role and daysIdle = 0', (done) => { - updatePermission('edit-other-user-active-status', ['admin']).then(() => { - request + void updatePermission('edit-other-user-active-status', ['admin']).then(() => { + void request .post(api('users.deactivateIdle')) .set(credentials) .send({ @@ -3334,8 +3486,8 @@ describe('[Users]', function () { }); }); it('should not deactivate the test user again when given its role and daysIdle = 0', (done) => { - updatePermission('edit-other-user-active-status', ['admin']).then(() => { - request + void updatePermission('edit-other-user-active-status', ['admin']).then(() => { + void request .post(api('users.deactivateIdle')) .set(credentials) .send({ @@ -3355,7 +3507,7 @@ describe('[Users]', function () { describe('[/users.requestDataDownload]', () => { it('should return the request data with fullExport false when no query parameter was send', (done) => { - request + void request .get(api('users.requestDataDownload')) .set(credentials) .expect('Content-Type', 'application/json') @@ -3369,8 +3521,9 @@ describe('[Users]', function () { .end(done); }); it('should return the request data with fullExport false when the fullExport query parameter is false', (done) => { - request - .get(api('users.requestDataDownload?fullExport=false')) + void request + .get(api('users.requestDataDownload')) + .query({ fullExport: 'false' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -3383,8 +3536,9 @@ describe('[Users]', function () { .end(done); }); it('should return the request data with fullExport true when the fullExport query parameter is true', (done) => { - request - .get(api('users.requestDataDownload?fullExport=true')) + void request + .get(api('users.requestDataDownload')) + .query({ fullExport: 'true' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -3399,9 +3553,9 @@ describe('[Users]', function () { }); describe('[/users.logoutOtherClients]', function () { - let user; - let userCredentials; - let newCredentials; + let user: TestUser; + let userCredentials: Credentials; + let newCredentials: Credentials; this.timeout(20000); @@ -3434,7 +3588,7 @@ describe('[Users]', function () { } } - request + void request .post(api('users.logoutOtherClients')) .set(newCredentials) .expect(200) @@ -3451,11 +3605,11 @@ describe('[Users]', function () { after(() => updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user'])); describe('[without permission]', function () { - let user; - let userCredentials; - let user2; - let user2Credentials; - let roomId; + let user: TestUser; + let userCredentials: Credentials; + let user2: TestUser; + let user2Credentials: Credentials; + let roomId: IRoom['_id']; this.timeout(20000); @@ -3478,8 +3632,9 @@ describe('[Users]', function () { }); it('should return an empty list when the user does not have any subscription', (done) => { - request - .get(api('users.autocomplete?selector={}')) + void request + .get(api('users.autocomplete')) + .query({ selector: '{}' }) .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200) @@ -3493,8 +3648,9 @@ describe('[Users]', function () { it('should return users that are subscribed to the same rooms as the requester', async () => { await joinChannel({ overrideCredentials: user2Credentials, roomId }); - request - .get(api('users.autocomplete?selector={}')) + void request + .get(api('users.autocomplete')) + .query({ selector: '{}' }) .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200) @@ -3509,10 +3665,10 @@ describe('[Users]', function () { before(() => updatePermission('view-outside-room', ['admin', 'user'])); it('should return an error when the required parameter "selector" is not provided', () => { - request + void request .get(api('users.autocomplete')) - .set(credentials) .query({}) + .set(credentials) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { @@ -3520,8 +3676,9 @@ describe('[Users]', function () { }); }); it('should return the users to fill auto complete', (done) => { - request - .get(api('users.autocomplete?selector={}')) + void request + .get(api('users.autocomplete')) + .query({ selector: '{}' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -3533,7 +3690,7 @@ describe('[Users]', function () { }); it('should filter results when using allowed operators', (done) => { - request + void request .get(api('users.autocomplete')) .set(credentials) .query({ @@ -3560,7 +3717,7 @@ describe('[Users]', function () { }); it('should return an error when using forbidden operators', (done) => { - request + void request .get(api('users.autocomplete')) .set(credentials) .query({ @@ -3593,7 +3750,7 @@ describe('[Users]', function () { describe('[/users.getStatus]', () => { it('should return my own status', (done) => { - request + void request .get(api('users.getStatus')) .set(credentials) .expect('Content-Type', 'application/json') @@ -3606,8 +3763,9 @@ describe('[Users]', function () { .end(done); }); it('should return other user status', (done) => { - request - .get(api('users.getStatus?userId=rocket.cat')) + void request + .get(api('users.getStatus')) + .query({ userId: 'rocket.cat' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -3621,7 +3779,7 @@ describe('[Users]', function () { }); describe('[/users.setStatus]', () => { - let user; + let user: TestUser; before(async () => { user = await createUser(); @@ -3629,8 +3787,8 @@ describe('[Users]', function () { after(() => Promise.all([deleteUser(user), updateSetting('Accounts_AllowUserStatusMessageChange', true)])); it('should return an error when the setting "Accounts_AllowUserStatusMessageChange" is disabled', (done) => { - updateSetting('Accounts_AllowUserStatusMessageChange', false).then(() => { - request + void updateSetting('Accounts_AllowUserStatusMessageChange', false).then(() => { + void request .post(api('users.setStatus')) .set(credentials) .send({ @@ -3648,8 +3806,8 @@ describe('[Users]', function () { }); }); it('should update my own status', (done) => { - updateSetting('Accounts_AllowUserStatusMessageChange', true).then(() => { - request + void updateSetting('Accounts_AllowUserStatusMessageChange', true).then(() => { + void request .post(api('users.setStatus')) .set(credentials) .send({ @@ -3660,14 +3818,14 @@ describe('[Users]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - getUserStatus(credentials['X-User-Id']).then((status) => expect(status.status).to.be.equal('busy')); + void getUserStatus(credentials['X-User-Id']).then((status) => expect(status.status).to.be.equal('busy')); }) .end(done); }); }); it('should return an error when trying to update other user status without the required permission', (done) => { - updatePermission('edit-other-user-info', []).then(() => { - request + void updatePermission('edit-other-user-info', []).then(() => { + void request .post(api('users.setStatus')) .set(credentials) .send({ @@ -3685,8 +3843,8 @@ describe('[Users]', function () { }); }); it('should update another user status succesfully', (done) => { - updatePermission('edit-other-user-info', ['admin']).then(() => { - request + void updatePermission('edit-other-user-info', ['admin']).then(() => { + void request .post(api('users.setStatus')) .set(credentials) .send({ @@ -3698,7 +3856,7 @@ describe('[Users]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - getUserStatus(credentials['X-User-Id']).then((status) => { + void getUserStatus(credentials['X-User-Id']).then((status) => { expect(status.status).to.be.equal('busy'); expect(status.message).to.be.equal('test'); }); @@ -3707,7 +3865,7 @@ describe('[Users]', function () { }); }); it('should return an error when the user try to update user status with an invalid status', (done) => { - request + void request .post(api('users.setStatus')) .set(credentials) .send({ @@ -3742,7 +3900,7 @@ describe('[Users]', function () { await updateSetting('Accounts_AllowInvisibleStatusOption', true); }); it('should return an error when the payload is missing all supported fields', (done) => { - request + void request .post(api('users.setStatus')) .set(credentials) .send({}) @@ -3757,9 +3915,9 @@ describe('[Users]', function () { }); describe('[/users.removeOtherTokens]', () => { - let user; - let userCredentials; - let newCredentials; + let user: TestUser; + let userCredentials: Credentials; + let newCredentials: Credentials; before(async () => { user = await createUser(); @@ -3790,17 +3948,17 @@ describe('[Users]', function () { } } - request.post(api('users.removeOtherTokens')).set(newCredentials).expect(200).then(tryAuthentication); + void request.post(api('users.removeOtherTokens')).set(newCredentials).expect(200).then(tryAuthentication); }); }); describe('[/users.listTeams]', () => { const teamName1 = `team-name-${Date.now()}`; const teamName2 = `team-name-2-${Date.now()}`; - let testUser; + let testUser: TestUser; before('create team 1', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -3818,7 +3976,7 @@ describe('[Users]', function () { }); before('create team 2', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -3840,7 +3998,7 @@ describe('[Users]', function () { }); before('add test user to team 1', (done) => { - request + void request .post(api('teams.addMembers')) .set(credentials) .send({ @@ -3861,7 +4019,7 @@ describe('[Users]', function () { }); before('add test user to team 2', (done) => { - request + void request .post(api('teams.addMembers')) .set(credentials) .send({ @@ -3884,7 +4042,7 @@ describe('[Users]', function () { after(() => Promise.all([...[teamName1, teamName2].map((team) => deleteTeam(credentials, team)), deleteUser(testUser)])); it('should list both channels', (done) => { - request + void request .get(api('users.listTeams')) .set(credentials) .query({ @@ -3906,9 +4064,9 @@ describe('[Users]', function () { }); describe('[/users.logout]', () => { - let user; - let otherUser; - let userCredentials; + let user: TestUser; + let otherUser: TestUser; + let userCredentials: Credentials; before(async () => { user = await createUser(); @@ -3919,8 +4077,8 @@ describe('[Users]', function () { after(() => Promise.all([deleteUser(user), deleteUser(otherUser), updatePermission('logout-other-user', ['admin'])])); it('should throw unauthorized error to user w/o "logout-other-user" permission', (done) => { - updatePermission('logout-other-user', []).then(() => { - request + void updatePermission('logout-other-user', []).then(() => { + void request .post(api('users.logout')) .set(credentials) .send({ userId: otherUser._id }) @@ -3931,8 +4089,8 @@ describe('[Users]', function () { }); it('should logout other user', (done) => { - updatePermission('logout-other-user', ['admin']).then(() => { - request + void updatePermission('logout-other-user', ['admin']).then(() => { + void request .post(api('users.logout')) .set(credentials) .send({ userId: otherUser._id }) @@ -3943,16 +4101,16 @@ describe('[Users]', function () { }); it('should logout the requester', (done) => { - updatePermission('logout-other-user', []).then(() => { - request.post(api('users.logout')).set(userCredentials).expect('Content-Type', 'application/json').expect(200).end(done); + void updatePermission('logout-other-user', []).then(() => { + void request.post(api('users.logout')).set(userCredentials).expect('Content-Type', 'application/json').expect(200).end(done); }); }); }); describe('[/users.listByStatus]', () => { - let user; - let otherUser; - let otherUserCredentials; + let user: TestUser; + let otherUser: TestUser; + let otherUserCredentials: Credentials; before(async () => { user = await createUser(); @@ -3977,7 +4135,9 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; + const { users } = res.body as PaginatedResult<{ + users: DefaultUserInfo[]; + }>; const ids = users.map((user) => user._id); expect(ids).to.include(user._id); }); @@ -3992,7 +4152,9 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; + const { users } = res.body as PaginatedResult<{ + users: DefaultUserInfo[]; + }>; const ids = users.map((user) => user._id); expect(ids).to.include(user._id); }); @@ -4009,7 +4171,9 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; + const { users } = res.body as PaginatedResult<{ + users: DefaultUserInfo[]; + }>; const ids = users.map((user) => user._id); expect(ids).to.include(user._id); }); @@ -4026,7 +4190,9 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; + const { users } = res.body as PaginatedResult<{ + users: DefaultUserInfo[]; + }>; const ids = users.map((user) => user._id); expect(ids).to.not.include(user._id); }); @@ -4047,7 +4213,9 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; + const { users } = res.body as PaginatedResult<{ + users: DefaultUserInfo[]; + }>; const ids = users.map((user) => user._id); expect(ids).to.include(user._id); }); @@ -4063,7 +4231,9 @@ describe('[Users]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('users'); - const { users } = res.body; + const { users } = res.body as PaginatedResult<{ + users: DefaultUserInfo[]; + }>; const ids = users.map((user) => user._id); expect(ids).to.include(user._id); }); @@ -4113,8 +4283,8 @@ describe('[Users]', function () { }); describe('[/users.sendWelcomeEmail]', async () => { - let user; - let otherUser; + let user: TestUser; + let otherUser: TestUser; before(async () => { user = await createUser(); diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.ts similarity index 92% rename from apps/meteor/tests/end-to-end/api/02-channels.js rename to apps/meteor/tests/end-to-end/api/02-channels.ts index ded521f52509c..193502195ef2b 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.ts @@ -1,7 +1,9 @@ -import { expect } from 'chai'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IIntegration, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { expect, assert } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials, reservedWords } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials, reservedWords } from '../../data/api-data'; import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; @@ -9,33 +11,31 @@ import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { deleteTeam } from '../../data/teams.helper'; import { testFileUploads } from '../../data/uploads.helper'; import { adminUsername, password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, login, deleteUser } from '../../data/users.helper'; -function getRoomInfo(roomId) { - return new Promise((resolve /* , reject*/) => { - request +function getRoomInfo(roomId: IRoom['_id']) { + return new Promise<{ channel: IRoom }>((resolve) => { + void request .get(api('channels.info')) .set(credentials) .query({ roomId, }) - .end((err, req) => { + .end((_err, req) => { resolve(req.body); }); }); } -const channel = {}; - -describe('[Channels]', function () { +describe('[Channels]', () => { + let channel: Pick; const apiPublicChannelName = `api-channel-test-${Date.now()}`; - this.retries(0); - before((done) => getCredentials(done)); before('Creating channel', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -49,8 +49,10 @@ describe('[Channels]', function () { expect(res.body).to.have.nested.property('channel.name', apiPublicChannelName); expect(res.body).to.have.nested.property('channel.t', 'c'); expect(res.body).to.have.nested.property('channel.msgs', 0); - channel._id = res.body.channel._id; - channel.name = res.body.channel.name; + channel = { + _id: res.body.channel._id, + name: res.body.channel.name, + }; }) .end(done); }); @@ -81,7 +83,7 @@ describe('[Channels]', function () { }); it('/channels.addModerator', (done) => { - request + void request .post(api('channels.addModerator')) .set(credentials) .send({ @@ -97,7 +99,7 @@ describe('[Channels]', function () { }); it('/channels.addModerator should fail with missing room Id', (done) => { - request + void request .post(api('channels.addModerator')) .set(credentials) .send({ @@ -112,7 +114,7 @@ describe('[Channels]', function () { }); it('/channels.addModerator should fail with missing user Id', (done) => { - request + void request .post(api('channels.addModerator')) .set(credentials) .send({ @@ -127,7 +129,7 @@ describe('[Channels]', function () { }); it('/channels.removeModerator', (done) => { - request + void request .post(api('channels.removeModerator')) .set(credentials) .send({ @@ -143,7 +145,7 @@ describe('[Channels]', function () { }); it('/channels.removeModerator should fail on invalid room id', (done) => { - request + void request .post(api('channels.removeModerator')) .set(credentials) .send({ @@ -158,7 +160,7 @@ describe('[Channels]', function () { }); it('/channels.removeModerator should fail on invalid user id', (done) => { - request + void request .post(api('channels.removeModerator')) .set(credentials) .send({ @@ -173,7 +175,7 @@ describe('[Channels]', function () { }); it('/channels.addOwner', (done) => { - request + void request .post(api('channels.addOwner')) .set(credentials) .send({ @@ -189,7 +191,7 @@ describe('[Channels]', function () { }); it('/channels.removeOwner', (done) => { - request + void request .post(api('channels.removeOwner')) .set(credentials) .send({ @@ -247,7 +249,7 @@ describe('[Channels]', function () { }); it('/channels.addOwner', (done) => { - request + void request .post(api('channels.addOwner')) .set(credentials) .send({ @@ -263,7 +265,7 @@ describe('[Channels]', function () { }); it('/channels.archive', (done) => { - request + void request .post(api('channels.archive')) .set(credentials) .send({ @@ -278,7 +280,7 @@ describe('[Channels]', function () { }); it('/channels.unarchive', (done) => { - request + void request .post(api('channels.unarchive')) .set(credentials) .send({ @@ -293,7 +295,7 @@ describe('[Channels]', function () { }); it('/channels.close', (done) => { - request + void request .post(api('channels.close')) .set(credentials) .send({ @@ -308,7 +310,7 @@ describe('[Channels]', function () { }); it('/channels.close', (done) => { - request + void request .post(api('channels.close')) .set(credentials) .send({ @@ -324,7 +326,7 @@ describe('[Channels]', function () { }); it('/channels.open', (done) => { - request + void request .post(api('channels.open')) .set(credentials) .send({ @@ -339,7 +341,7 @@ describe('[Channels]', function () { }); it('/channels.list', (done) => { - request + void request .get(api('channels.list')) .set(credentials) .query({ @@ -356,7 +358,7 @@ describe('[Channels]', function () { }); it('/channels.list.joined', (done) => { - request + void request .get(api('channels.list.joined')) .set(credentials) .query({ @@ -372,7 +374,7 @@ describe('[Channels]', function () { .end(done); }); it('/channels.counters', (done) => { - request + void request .get(api('channels.counters')) .set(credentials) .query({ @@ -396,9 +398,9 @@ describe('[Channels]', function () { it('/channels.rename', async () => { const roomInfo = await getRoomInfo(channel._id); - function failRenameChannel(name) { + function failRenameChannel(name: string) { it(`should not rename a channel to the reserved name ${name}`, (done) => { - request + void request .post(api('channels.rename')) .set(credentials) .send({ @@ -438,7 +440,7 @@ describe('[Channels]', function () { }); it('/channels.addAll', (done) => { - request + void request .post(api('channels.addAll')) .set(credentials) .send({ @@ -456,7 +458,7 @@ describe('[Channels]', function () { }); it('/channels.addLeader', (done) => { - request + void request .post(api('channels.addLeader')) .set(credentials) .send({ @@ -471,7 +473,7 @@ describe('[Channels]', function () { .end(done); }); it('/channels.removeLeader', (done) => { - request + void request .post(api('channels.removeLeader')) .set(credentials) .send({ @@ -548,8 +550,8 @@ describe('[Channels]', function () { }); describe('[/channels.create]', () => { - let guestUser; - let room; + let guestUser: TestUser; + let room: IRoom; before(async () => { guestUser = await createUser({ roles: ['guest'] }); @@ -592,7 +594,7 @@ describe('[Channels]', function () { } const channelIds = (await Promise.all(promises)).map((r) => r.body.channel).map((channel) => channel._id); - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -606,7 +608,7 @@ describe('[Channels]', function () { room = res.body.group; }) .then(() => { - request + void request .get(api('channels.members')) .set(credentials) .query({ @@ -626,15 +628,14 @@ describe('[Channels]', function () { }); describe('[/channels.info]', () => { const testChannelName = `api-channel-test-${Date.now()}`; - let testChannel = {}; - let channelMessage = {}; + let testChannel: IRoom; after(async () => { await deleteRoom({ type: 'c', roomId: testChannel._id }); }); it('creating new channel...', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -648,7 +649,7 @@ describe('[Channels]', function () { .end(done); }); it('should fail to create the same channel twice', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -663,7 +664,7 @@ describe('[Channels]', function () { .end(done); }); it('should return channel basic structure', (done) => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -680,8 +681,11 @@ describe('[Channels]', function () { }) .end(done); }); + + let channelMessage: IMessage; + it('sending a message...', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -699,7 +703,7 @@ describe('[Channels]', function () { .end(done); }); it('REACTing with last message', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -714,7 +718,7 @@ describe('[Channels]', function () { .end(done); }); it('STARring last message', (done) => { - request + void request .post(api('chat.starMessage')) .set(credentials) .send({ @@ -728,7 +732,7 @@ describe('[Channels]', function () { .end(done); }); it('PINning last message', (done) => { - request + void request .post(api('chat.pinMessage')) .set(credentials) .send({ @@ -742,7 +746,7 @@ describe('[Channels]', function () { .end(done); }); it('should return channel structure with "lastMessage" object including pin, reactions and star(should be an array) infos', (done) => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -764,7 +768,7 @@ describe('[Channels]', function () { .end(done); }); it('should return all channels messages where the last message of array should have the "star" array with USERS star ONLY', (done) => { - request + void request .get(api('channels.messages')) .set(credentials) .query({ @@ -775,15 +779,15 @@ describe('[Channels]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - const { messages } = res.body; + const messages = res.body.messages as IMessage[]; const lastMessage = messages.filter((message) => message._id === channelMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); - expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); + expect(lastMessage.starred?.[0]._id).to.be.equal(adminUsername); }) .end(done); }); it('should return all channels messages where the last message of array should have the "star" array with USERS star ONLY even requested with count and offset params', (done) => { - request + void request .get(api('channels.messages')) .set(credentials) .query({ @@ -796,18 +800,18 @@ describe('[Channels]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - const { messages } = res.body; + const messages = res.body.messages as IMessage[]; const lastMessage = messages.filter((message) => message._id === channelMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); - expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); + expect(lastMessage.starred?.[0]._id).to.be.equal(adminUsername); }) .end(done); }); }); describe('[/channels.online]', () => { - const createdChannels = []; - const createdUsers = []; + const createdChannels: IRoom[] = []; + const createdUsers: TestUser[] = []; const createUserAndChannel = async () => { const testUser = await createUser(); @@ -843,7 +847,7 @@ describe('[Channels]', function () { }); it('should return an error if no query', () => - request + void request .get(api('channels.online')) .set(credentials) .expect('Content-Type', 'application/json') @@ -854,7 +858,7 @@ describe('[Channels]', function () { })); it('should return an error if passing an empty query', () => - request + void request .get(api('channels.online')) .set(credentials) .query('query={}') @@ -916,10 +920,10 @@ describe('[Channels]', function () { }); describe('[/channels.join]', () => { - let testChannelNoCode; - let testChannelWithCode; - let testUser; - let testUserCredentials; + let testChannelNoCode: IRoom; + let testChannelWithCode: IRoom; + let testUser: TestUser; + let testUserCredentials: Credentials; before('Create test user', async () => { testUser = await createUser(); @@ -943,7 +947,7 @@ describe('[Channels]', function () { }); before('Set code for channel', (done) => { - request + void request .post(api('channels.setJoinCode')) .set(testUserCredentials) .send({ @@ -959,7 +963,7 @@ describe('[Channels]', function () { }); it('should fail if invalid channel', (done) => { - request + void request .post(api('channels.join')) .set(credentials) .send({ @@ -976,7 +980,7 @@ describe('[Channels]', function () { describe('code-free channel', () => { it('should succeed when joining code-free channel without join code', (done) => { - request + void request .post(api('channels.join')) .set(credentials) .send({ @@ -999,7 +1003,7 @@ describe('[Channels]', function () { }); it('should fail when joining code-needed channel without join code and no join-without-join-code permission', (done) => { - request + void request .post(api('channels.join')) .set(credentials) .send({ @@ -1015,7 +1019,7 @@ describe('[Channels]', function () { }); it('should fail when joining code-needed channel with incorrect join code and no join-without-join-code permission', (done) => { - request + void request .post(api('channels.join')) .set(credentials) .send({ @@ -1032,7 +1036,7 @@ describe('[Channels]', function () { }); it('should succeed when joining code-needed channel with join code', (done) => { - request + void request .post(api('channels.join')) .set(credentials) .send({ @@ -1055,7 +1059,7 @@ describe('[Channels]', function () { }); before('leave channel', (done) => { - request + void request .post(api('channels.leave')) .set(credentials) .send({ @@ -1070,7 +1074,7 @@ describe('[Channels]', function () { }); it('should succeed when joining code-needed channel without join code and with join-without-join-code permission', (done) => { - request + void request .post(api('channels.join')) .set(credentials) .send({ @@ -1090,7 +1094,7 @@ describe('[Channels]', function () { describe('/channels.setDescription', () => { it('should set the description of the channel with a string', (done) => { - request + void request .post(api('channels.setDescription')) .set(credentials) .send({ @@ -1106,7 +1110,7 @@ describe('[Channels]', function () { .end(done); }); it('should set the description of the channel with an empty string(remove the description)', (done) => { - request + void request .post(api('channels.setDescription')) .set(credentials) .send({ @@ -1125,7 +1129,7 @@ describe('[Channels]', function () { describe('/channels.setTopic', () => { it('should set the topic of the channel with a string', (done) => { - request + void request .post(api('channels.setTopic')) .set(credentials) .send({ @@ -1141,7 +1145,7 @@ describe('[Channels]', function () { .end(done); }); it('should set the topic of the channel with an empty string(remove the topic)', (done) => { - request + void request .post(api('channels.setTopic')) .set(credentials) .send({ @@ -1160,7 +1164,7 @@ describe('[Channels]', function () { describe('/channels.setAnnouncement', () => { it('should set the announcement of the channel with a string', (done) => { - request + void request .post(api('channels.setAnnouncement')) .set(credentials) .send({ @@ -1176,7 +1180,7 @@ describe('[Channels]', function () { .end(done); }); it('should set the announcement of the channel with an empty string(remove the announcement)', (done) => { - request + void request .post(api('channels.setAnnouncement')) .set(credentials) .send({ @@ -1195,7 +1199,7 @@ describe('[Channels]', function () { describe('/channels.setPurpose', () => { it('should set the purpose of the channel with a string', (done) => { - request + void request .post(api('channels.setPurpose')) .set(credentials) .send({ @@ -1211,7 +1215,7 @@ describe('[Channels]', function () { .end(done); }); it('should set the announcement of channel with an empty string(remove the purpose)', (done) => { - request + void request .post(api('channels.setPurpose')) .set(credentials) .send({ @@ -1230,7 +1234,7 @@ describe('[Channels]', function () { describe('/channels.history', () => { it('should return an array of members by channel', (done) => { - request + void request .get(api('channels.history')) .set(credentials) .query({ @@ -1246,7 +1250,7 @@ describe('[Channels]', function () { }); it('should return an array of members by channel even requested with count and offset params', (done) => { - request + void request .get(api('channels.history')) .set(credentials) .query({ @@ -1265,7 +1269,7 @@ describe('[Channels]', function () { }); describe('/channels.members', () => { - let testUser; + let testUser: TestUser; before(async () => { testUser = await createUser(); @@ -1286,7 +1290,7 @@ describe('[Channels]', function () { }); it('should return an array of members by channel', (done) => { - request + void request .get(api('channels.members')) .set(credentials) .query({ @@ -1305,7 +1309,7 @@ describe('[Channels]', function () { }); it('should return an array of members by channel even requested with count and offset params', (done) => { - request + void request .get(api('channels.members')) .set(credentials) .query({ @@ -1326,7 +1330,7 @@ describe('[Channels]', function () { }); it('should return an filtered array of members by channel', (done) => { - request + void request .get(api('channels.members')) .set(credentials) .query({ @@ -1349,21 +1353,21 @@ describe('[Channels]', function () { }); describe('/channels.getIntegrations', () => { - let integrationCreatedByAnUser; - let userCredentials; - let createdChannel; - let user; + let integrationCreatedByAnUser: IIntegration; + let userCredentials: Credentials; + let createdChannel: IRoom; + let user: TestUser; before((done) => { - createRoom({ name: `test-integration-channel-${Date.now()}`, type: 'c' }).end((err, res) => { + void createRoom({ name: `test-integration-channel-${Date.now()}`, type: 'c' }).end((_err, res) => { createdChannel = res.body.channel; - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(user.username, password).then((credentials) => { + void login(user.username, password).then((credentials) => { userCredentials = credentials; - updatePermission('manage-incoming-integrations', ['user']).then(() => { - updatePermission('manage-own-incoming-integrations', ['user']).then(() => { - createIntegration( + void updatePermission('manage-incoming-integrations', ['user']).then(() => { + void updatePermission('manage-own-incoming-integrations', ['user']).then(() => { + void createIntegration( { type: 'webhook-incoming', name: 'Incoming test', @@ -1397,8 +1401,8 @@ describe('[Channels]', function () { }); it('should return the list of integrations of created channel and it should contain the integration created by user when the admin DOES have the permission', (done) => { - updatePermission('manage-incoming-integrations', ['admin']).then(() => { - request + void updatePermission('manage-incoming-integrations', ['admin']).then(() => { + void request .get(api('channels.getIntegrations')) .set(credentials) .query({ @@ -1408,9 +1412,10 @@ describe('[Channels]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( + const integrationCreated = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, ); + assert.isDefined(integrationCreated); expect(integrationCreated).to.be.an('object'); expect(integrationCreated._id).to.be.equal(integrationCreatedByAnUser._id); expect(res.body).to.have.property('offset'); @@ -1421,9 +1426,9 @@ describe('[Channels]', function () { }); it('should return the list of integrations created by the user only', (done) => { - updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - updatePermission('manage-incoming-integrations', []).then(() => { - request + void updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { + void updatePermission('manage-incoming-integrations', []).then(() => { + void request .get(api('channels.getIntegrations')) .set(credentials) .query({ @@ -1433,9 +1438,10 @@ describe('[Channels]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( + const integrationCreated = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, ); + assert.isUndefined(integrationCreated); expect(integrationCreated).to.be.equal(undefined); expect(res.body).to.have.property('offset'); expect(res.body).to.have.property('total'); @@ -1446,11 +1452,11 @@ describe('[Channels]', function () { }); it('should return unauthorized error when the user does not have any integrations permissions', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-own-outgoing-integrations', []).then(() => { - request + void updatePermission('manage-incoming-integrations', []).then(() => { + void updatePermission('manage-own-incoming-integrations', []).then(() => { + void updatePermission('manage-outgoing-integrations', []).then(() => { + void updatePermission('manage-own-outgoing-integrations', []).then(() => { + void request .get(api('channels.getIntegrations')) .set(credentials) .query({ @@ -1471,8 +1477,8 @@ describe('[Channels]', function () { }); describe('/channels.setCustomFields:', () => { - let withCFChannel; - let withoutCFChannel; + let withCFChannel: IRoom; + let withoutCFChannel: IRoom; after(async () => { await deleteRoom({ type: 'c', roomId: withCFChannel._id }); @@ -1480,20 +1486,20 @@ describe('[Channels]', function () { it('create channel with customFields', (done) => { const customFields = { field0: 'value0' }; - request + void request .post(api('channels.create')) .set(credentials) .send({ name: `channel.cf.${Date.now()}`, customFields, }) - .end((err, res) => { + .end((_err, res) => { withCFChannel = res.body.channel; done(); }); }); it('get customFields using channels.info', (done) => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -1528,7 +1534,7 @@ describe('[Channels]', function () { }); }); it('get customFields using channels.info', (done) => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -1543,7 +1549,7 @@ describe('[Channels]', function () { .end(done); }); it('delete channels with customFields', (done) => { - request + void request .post(api('channels.delete')) .set(credentials) .send({ @@ -1557,13 +1563,13 @@ describe('[Channels]', function () { .end(done); }); it('create channel without customFields', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ name: `channel.cf.${Date.now()}`, }) - .end((err, res) => { + .end((_err, res) => { withoutCFChannel = res.body.channel; done(); }); @@ -1613,7 +1619,7 @@ describe('[Channels]', function () { it('set customFields to empty object', (done) => { const customFields = {}; - request + void request .post(api('channels.setCustomFields')) .set(credentials) .send({ @@ -1636,7 +1642,7 @@ describe('[Channels]', function () { it('set customFields as a string -> should return 400', (done) => { const customFields = ''; - request + void request .post(api('channels.setCustomFields')) .set(credentials) .send({ @@ -1651,7 +1657,7 @@ describe('[Channels]', function () { .end(done); }); it('delete channel with empty customFields', (done) => { - request + void request .post(api('channels.delete')) .set(credentials) .send({ @@ -1667,7 +1673,7 @@ describe('[Channels]', function () { }); describe('/channels.setDefault', () => { - let testChannel; + let testChannel: IRoom; const name = `setDefault-${Date.now()}`; before(async () => { @@ -1723,7 +1729,7 @@ describe('[Channels]', function () { }); describe('/channels.setType', () => { - let testChannel; + let testChannel: IRoom; const name = `setType-${Date.now()}`; before(async () => { @@ -1737,7 +1743,7 @@ describe('[Channels]', function () { it('should change the type public channel to private', async () => { const roomInfo = await getRoomInfo(testChannel._id); - request + void request .post(api('channels.setType')) .set(credentials) .send({ @@ -1757,7 +1763,7 @@ describe('[Channels]', function () { }); describe('/channels.delete:', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel; @@ -1768,7 +1774,7 @@ describe('[Channels]', function () { }); it('/channels.delete', (done) => { - request + void request .post(api('channels.delete')) .set(credentials) .send({ @@ -1782,7 +1788,7 @@ describe('[Channels]', function () { .end(done); }); it('/channels.info', (done) => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -1800,7 +1806,7 @@ describe('[Channels]', function () { describe('/channels.getAllUserMentionsByChannel', () => { it('should return an array of mentions by channel', (done) => { - request + void request .get(api('channels.getAllUserMentionsByChannel')) .set(credentials) .query({ @@ -1818,7 +1824,7 @@ describe('[Channels]', function () { .end(done); }); it('should return an array of mentions by channel even requested with count and offset params', (done) => { - request + void request .get(api('channels.getAllUserMentionsByChannel')) .set(credentials) .query({ @@ -1840,7 +1846,7 @@ describe('[Channels]', function () { }); describe('/channels.roles', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.roles.test.${Date.now()}` })).body.channel; @@ -1851,7 +1857,7 @@ describe('[Channels]', function () { }); it('/channels.invite', (done) => { - request + void request .post(api('channels.invite')) .set(credentials) .send({ @@ -1861,7 +1867,7 @@ describe('[Channels]', function () { .end(done); }); it('/channels.addModerator', (done) => { - request + void request .post(api('channels.addModerator')) .set(credentials) .send({ @@ -1871,7 +1877,7 @@ describe('[Channels]', function () { .end(done); }); it('/channels.addLeader', (done) => { - request + void request .post(api('channels.addLeader')) .set(credentials) .send({ @@ -1881,7 +1887,7 @@ describe('[Channels]', function () { .end(done); }); it('should return an array of role <-> user relationships in a channel', (done) => { - request + void request .get(api('channels.roles')) .set(credentials) .query({ @@ -1912,7 +1918,7 @@ describe('[Channels]', function () { }); describe('/channels.moderators', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.moderators.test.${Date.now()}` })).body.channel; @@ -1923,7 +1929,7 @@ describe('[Channels]', function () { }); it('/channels.invite', (done) => { - request + void request .post(api('channels.invite')) .set(credentials) .send({ @@ -1933,7 +1939,7 @@ describe('[Channels]', function () { .end(done); }); it('/channels.addModerator', (done) => { - request + void request .post(api('channels.addModerator')) .set(credentials) .send({ @@ -1943,7 +1949,7 @@ describe('[Channels]', function () { .end(done); }); it('should return an array of moderators with rocket.cat as a moderator', (done) => { - request + void request .get(api('channels.moderators')) .set(credentials) .query({ @@ -1961,7 +1967,7 @@ describe('[Channels]', function () { }); describe('/channels.anonymousread', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.anonymousread.test.${Date.now()}` })).body.channel; @@ -1972,8 +1978,8 @@ describe('[Channels]', function () { }); it('should return an error when the setting "Accounts_AllowAnonymousRead" is disabled', (done) => { - updateSetting('Accounts_AllowAnonymousRead', false).then(() => { - request + void updateSetting('Accounts_AllowAnonymousRead', false).then(() => { + void request .get(api('channels.anonymousread')) .query({ roomId: testChannel._id, @@ -1991,8 +1997,8 @@ describe('[Channels]', function () { }); }); it('should return the messages list when the setting "Accounts_AllowAnonymousRead" is enabled', (done) => { - updateSetting('Accounts_AllowAnonymousRead', true).then(() => { - request + void updateSetting('Accounts_AllowAnonymousRead', true).then(() => { + void request .get(api('channels.anonymousread')) .query({ roomId: testChannel._id, @@ -2007,8 +2013,8 @@ describe('[Channels]', function () { }); }); it('should return the messages list when the setting "Accounts_AllowAnonymousRead" is enabled even requested with count and offset params', (done) => { - updateSetting('Accounts_AllowAnonymousRead', true).then(() => { - request + void updateSetting('Accounts_AllowAnonymousRead', true).then(() => { + void request .get(api('channels.anonymousread')) .query({ roomId: testChannel._id, @@ -2027,13 +2033,14 @@ describe('[Channels]', function () { }); describe('/channels.convertToTeam', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.convertToTeam.test.${Date.now()}` })).body.channel; }); after(async () => { + assert.isDefined(testChannel.name); await Promise.all([ updatePermission('create-team', ['admin', 'user']), updatePermission('edit-room', ['admin', 'owner', 'moderator']), @@ -2070,7 +2077,7 @@ describe('[Channels]', function () { }); it(`should return an error when the channel's name and id are sent as parameter`, (done) => { - request + void request .post(api('channels.convertToTeam')) .set(credentials) .send({ @@ -2120,11 +2127,11 @@ describe('[Channels]', function () { }); it('should fail to convert channel without the required parameters', (done) => { - request.post(api('channels.convertToTeam')).set(credentials).send({}).expect(400).end(done); + void request.post(api('channels.convertToTeam')).set(credentials).send({}).expect(400).end(done); }); it("should fail to convert channel if it's already taken", (done) => { - request + void request .post(api('channels.convertToTeam')) .set(credentials) .send({ channelId: testChannel._id }) @@ -2137,7 +2144,7 @@ describe('[Channels]', function () { }); describe("Setting: 'Use Real Name': true", () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.anonymousread.test.${Date.now()}` })).body.channel; @@ -2183,7 +2190,7 @@ describe('[Channels]', function () { }); it('/channels.list', (done) => { - request + void request .get(api('channels.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2194,7 +2201,7 @@ describe('[Channels]', function () { expect(res.body).to.have.property('total'); expect(res.body).to.have.property('channels').and.to.be.an('array'); - const retChannel = res.body.channels.find(({ _id }) => _id === testChannel._id); + const retChannel = (res.body.channels as IRoom[]).find(({ _id }) => _id === testChannel._id); expect(retChannel).to.have.nested.property('lastMessage.u.name', 'RocketChat Internal Admin Test'); }) @@ -2202,7 +2209,7 @@ describe('[Channels]', function () { }); it('/channels.list.joined', (done) => { - request + void request .get(api('channels.list.joined')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2213,7 +2220,7 @@ describe('[Channels]', function () { expect(res.body).to.have.property('total'); expect(res.body).to.have.property('channels').and.to.be.an('array'); - const retChannel = res.body.channels.find(({ _id }) => _id === testChannel._id); + const retChannel = (res.body.channels as IRoom[]).find(({ _id }) => _id === testChannel._id); expect(retChannel).to.have.nested.property('lastMessage.u.name', 'RocketChat Internal Admin Test'); }) diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.ts similarity index 94% rename from apps/meteor/tests/end-to-end/api/03-groups.js rename to apps/meteor/tests/end-to-end/api/03-groups.ts index 2bf6bc7749995..702a8bf5f8400 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.ts @@ -1,7 +1,9 @@ -import { expect } from 'chai'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IIntegration, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { assert, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials, group, apiPrivateChannelName } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials, apiPrivateChannelName } from '../../data/api-data'; import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; @@ -9,24 +11,28 @@ import { createRoom } from '../../data/rooms.helper'; import { deleteTeam } from '../../data/teams.helper'; import { testFileUploads } from '../../data/uploads.helper'; import { adminUsername, password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, login, deleteUser } from '../../data/users.helper'; -function getRoomInfo(roomId) { - return new Promise((resolve /* , reject*/) => { - request +function getRoomInfo(roomId: IRoom['_id']) { + return new Promise<{ group: IRoom }>((resolve) => { + void request .get(api('groups.info')) .set(credentials) .query({ roomId, }) - .end((err, req) => { + .end((_err, req) => { resolve(req.body); }); }); } -describe('[Groups]', function () { - this.retries(0); +describe('[Groups]', () => { + let group: { + _id: string; + name: string; + }; before((done) => getCredentials(done)); @@ -45,8 +51,10 @@ describe('[Groups]', function () { expect(res.body).to.have.nested.property('group.name', apiPrivateChannelName); expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.msgs', 0); - group._id = res.body.group._id; - group.name = res.body.group.name; + group = { + _id: res.body.group._id, + name: res.body.group.name, + }; }); }); @@ -62,12 +70,13 @@ describe('[Groups]', function () { }); describe('/groups.create', () => { - let guestUser; - let room; + let guestUser: TestUser; + let room: IRoom; before(async () => { guestUser = await createUser({ roles: ['guest'] }); }); + after(async () => { await deleteUser(guestUser); }); @@ -131,7 +140,7 @@ describe('[Groups]', function () { await Promise.all([updateSetting('E2E_Enable', false), updateSetting('E2E_Allow_Unencrypted_Messages', true)]); }); - let rid; + let rid: IRoom['_id']; it('should create a new encrypted group', async () => { await request @@ -262,8 +271,7 @@ describe('[Groups]', function () { }); describe('/groups.info', () => { - let testGroup = {}; - let groupMessage = {}; + let testGroup: IRoom; const newGroupInfoName = `info-private-channel-test-${Date.now()}`; @@ -310,6 +318,8 @@ describe('[Groups]', function () { }); }); + let groupMessage: IMessage; + it('sending a message...', async () => { await request .post(api('chat.sendMessage')) @@ -404,10 +414,10 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - const { messages } = res.body; + const messages = res.body.messages as IMessage[]; const lastMessage = messages.filter((message) => message._id === groupMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); - expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); + expect(lastMessage.starred?.[0]._id).to.be.equal(adminUsername); }); }); @@ -425,16 +435,16 @@ describe('[Groups]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - const { messages } = res.body; + const messages = res.body.messages as IMessage[]; const lastMessage = messages.filter((message) => message._id === groupMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); - expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); + expect(lastMessage.starred?.[0]._id).to.be.equal(adminUsername); }); }); }); describe('/groups.invite', async () => { - let roomInfo = {}; + let roomInfo: { group: IRoom }; before(async () => { roomInfo = await getRoomInfo(group._id); @@ -462,7 +472,7 @@ describe('[Groups]', function () { describe('/groups.addModerator', () => { it('should make user a moderator', (done) => { - request + void request .post(api('groups.addModerator')) .set(credentials) .send({ @@ -480,7 +490,7 @@ describe('[Groups]', function () { describe('/groups.removeModerator', () => { it('should remove user from moderator', (done) => { - request + void request .post(api('groups.removeModerator')) .set(credentials) .send({ @@ -498,7 +508,7 @@ describe('[Groups]', function () { describe('/groups.addOwner', () => { it('should add user as owner', (done) => { - request + void request .post(api('groups.addOwner')) .set(credentials) .send({ @@ -516,7 +526,7 @@ describe('[Groups]', function () { describe('/groups.removeOwner', () => { it('should remove user from owner', (done) => { - request + void request .post(api('groups.removeOwner')) .set(credentials) .send({ @@ -534,7 +544,7 @@ describe('[Groups]', function () { describe('/groups.addLeader', () => { it('should add user as leader', (done) => { - request + void request .post(api('groups.addLeader')) .set(credentials) .send({ @@ -552,7 +562,7 @@ describe('[Groups]', function () { describe('/groups.removeLeader', () => { it('should remove user from leader', (done) => { - request + void request .post(api('groups.removeLeader')) .set(credentials) .send({ @@ -569,14 +579,14 @@ describe('[Groups]', function () { }); describe('/groups.kick', () => { - let testUserModerator; - let credsModerator; - let testUserOwner; - let credsOwner; - let testUserMember; - let groupTest; - - const inviteUser = async (userId) => { + let testUserModerator: TestUser; + let credsModerator: Credentials; + let testUserOwner: TestUser; + let credsOwner: Credentials; + let testUserMember: TestUser; + let groupTest: IRoom; + + const inviteUser = async (userId: IUser['_id']) => { await request .post(api('groups.invite')) .set(credsOwner) @@ -748,7 +758,7 @@ describe('[Groups]', function () { describe('/groups.setDescription', () => { it('should set the description of the group with a string', (done) => { - request + void request .post(api('groups.setDescription')) .set(credentials) .send({ @@ -764,7 +774,7 @@ describe('[Groups]', function () { .end(done); }); it('should set the description of the group with an empty string(remove the description)', (done) => { - request + void request .post(api('groups.setDescription')) .set(credentials) .send({ @@ -783,7 +793,7 @@ describe('[Groups]', function () { describe('/groups.setTopic', () => { it('should set the topic of the group with a string', (done) => { - request + void request .post(api('groups.setTopic')) .set(credentials) .send({ @@ -799,7 +809,7 @@ describe('[Groups]', function () { .end(done); }); it('should set the topic of the group with an empty string(remove the topic)', (done) => { - request + void request .post(api('groups.setTopic')) .set(credentials) .send({ @@ -818,7 +828,7 @@ describe('[Groups]', function () { describe('/groups.setPurpose', () => { it('should set the purpose of the group with a string', (done) => { - request + void request .post(api('groups.setPurpose')) .set(credentials) .send({ @@ -834,7 +844,7 @@ describe('[Groups]', function () { .end(done); }); it('should set the purpose of the group with an empty string(remove the purpose)', (done) => { - request + void request .post(api('groups.setPurpose')) .set(credentials) .send({ @@ -853,7 +863,7 @@ describe('[Groups]', function () { describe('/groups.history', () => { it('should return groups history when searching by roomId', (done) => { - request + void request .get(api('groups.history')) .set(credentials) .query({ @@ -868,7 +878,7 @@ describe('[Groups]', function () { .end(done); }); it('should return groups history when searching by roomId even requested with count and offset params', (done) => { - request + void request .get(api('groups.history')) .set(credentials) .query({ @@ -888,7 +898,7 @@ describe('[Groups]', function () { describe('/groups.archive', () => { it('should archive the group', (done) => { - request + void request .post(api('groups.archive')) .set(credentials) .send({ @@ -905,7 +915,7 @@ describe('[Groups]', function () { describe('/groups.unarchive', () => { it('should unarchive the group', (done) => { - request + void request .post(api('groups.unarchive')) .set(credentials) .send({ @@ -922,7 +932,7 @@ describe('[Groups]', function () { describe('/groups.close', () => { it('should close the group', (done) => { - request + void request .post(api('groups.close')) .set(credentials) .send({ @@ -937,7 +947,7 @@ describe('[Groups]', function () { }); it('should return an error when trying to close a private group that is already closed', (done) => { - request + void request .post(api('groups.close')) .set(credentials) .send({ @@ -955,7 +965,7 @@ describe('[Groups]', function () { describe('/groups.open', () => { it('should open the group', (done) => { - request + void request .post(api('groups.open')) .set(credentials) .send({ @@ -972,7 +982,7 @@ describe('[Groups]', function () { describe('/groups.list', () => { it('should list the groups the caller is part of', (done) => { - request + void request .get(api('groups.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -1099,7 +1109,7 @@ describe('[Groups]', function () { describe('/groups.members', () => { it('should return group members when searching by roomId', (done) => { - request + void request .get(api('groups.members')) .set(credentials) .query({ @@ -1117,7 +1127,7 @@ describe('[Groups]', function () { .end(done); }); it('should return group members when searching by roomId even requested with count and offset params', (done) => { - request + void request .get(api('groups.members')) .set(credentials) .query({ @@ -1144,8 +1154,8 @@ describe('[Groups]', function () { describe('/groups.listAll', () => { it('should fail if the user doesnt have view-room-administration permission', (done) => { - updatePermission('view-room-administration', []).then(() => { - request + void updatePermission('view-room-administration', []).then(() => { + void request .get(api('groups.listAll')) .set(credentials) .expect('Content-Type', 'application/json') @@ -1158,8 +1168,8 @@ describe('[Groups]', function () { }); }); it('should succeed if user has view-room-administration permission', (done) => { - updatePermission('view-room-administration', ['admin']).then(() => { - request + void updatePermission('view-room-administration', ['admin']).then(() => { + void request .get(api('groups.listAll')) .set(credentials) .expect('Content-Type', 'application/json') @@ -1175,7 +1185,7 @@ describe('[Groups]', function () { describe('/groups.counters', () => { it('should return group counters', (done) => { - request + void request .get(api('groups.counters')) .set(credentials) .query({ @@ -1198,7 +1208,7 @@ describe('[Groups]', function () { }); describe('/groups.rename', async () => { - let roomInfo; + let roomInfo: { group: IRoom }; before(async () => { roomInfo = await getRoomInfo(group._id); }); @@ -1224,8 +1234,8 @@ describe('[Groups]', function () { }); describe('/groups.getIntegrations', () => { - let integrationCreatedByAnUser; - let createdGroup; + let integrationCreatedByAnUser: IIntegration; + let createdGroup: IRoom; before(async () => { const resRoom = await createRoom({ name: `test-integration-group-${Date.now()}`, type: 'p' }); @@ -1280,9 +1290,10 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( + const integrationCreated = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, ); + assert.isDefined(integrationCreated); expect(integrationCreated).to.be.an('object'); expect(integrationCreated._id).to.be.equal(integrationCreatedByAnUser._id); expect(res.body).to.have.property('offset'); @@ -1306,7 +1317,7 @@ describe('[Groups]', function () { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreated = res.body.integrations.find( + const integrationCreated = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integrationCreatedByAnUser._id, ); expect(integrationCreated).to.be.equal(undefined); @@ -1340,7 +1351,7 @@ describe('[Groups]', function () { describe('/groups.setReadOnly', () => { it('should set the group as read only', (done) => { - request + void request .post(api('groups.setReadOnly')) .set(credentials) .send({ @@ -1358,7 +1369,7 @@ describe('[Groups]', function () { describe.skip('/groups.leave', () => { it('should allow the user to leave the group', (done) => { - request + void request .post(api('groups.leave')) .set(credentials) .send({ @@ -1375,7 +1386,7 @@ describe('[Groups]', function () { describe('/groups.setAnnouncement', () => { it('should set the announcement of the group with a string', (done) => { - request + void request .post(api('groups.setAnnouncement')) .set(credentials) .send({ @@ -1391,7 +1402,7 @@ describe('[Groups]', function () { .end(done); }); it('should set the announcement of the group with an empty string(remove the announcement)', (done) => { - request + void request .post(api('groups.setAnnouncement')) .set(credentials) .send({ @@ -1409,7 +1420,7 @@ describe('[Groups]', function () { }); describe('/groups.setType', () => { - let roomTypeId; + let roomTypeId: IRoom['_id']; before(async () => { await request @@ -1456,8 +1467,8 @@ describe('[Groups]', function () { }); describe('/groups.setCustomFields', () => { - let cfchannel; - let groupWithoutCustomFields; + let cfchannel: IRoom; + let groupWithoutCustomFields: IRoom; before('create group with customFields', async () => { const customFields = { field0: 'value0' }; @@ -1505,7 +1516,7 @@ describe('[Groups]', function () { }); it('get customFields using groups.info', (done) => { - request + void request .get(api('groups.info')) .set(credentials) .query({ @@ -1540,7 +1551,7 @@ describe('[Groups]', function () { }); }); it('get customFields using groups.info', (done) => { - request + void request .get(api('groups.info')) .set(credentials) .query({ @@ -1622,7 +1633,7 @@ describe('[Groups]', function () { it('set customFields as a string -> should return 400', (done) => { const customFields = ''; - request + void request .post(api('groups.setCustomFields')) .set(credentials) .send({ @@ -1639,7 +1650,7 @@ describe('[Groups]', function () { }); describe('/groups.delete', () => { - let testGroup; + let testGroup: IRoom; before(async () => { await request .post(api('groups.create')) @@ -1655,7 +1666,7 @@ describe('[Groups]', function () { }); it('should delete group', (done) => { - request + void request .post(api('groups.delete')) .set(credentials) .send({ @@ -1670,7 +1681,7 @@ describe('[Groups]', function () { }); it('should return group not found', (done) => { - request + void request .get(api('groups.info')) .set(credentials) .query({ @@ -1687,7 +1698,7 @@ describe('[Groups]', function () { }); describe('/groups.roles', () => { - let testGroup; + let testGroup: IRoom; before(async () => { await request .post(api('groups.create')) @@ -1714,7 +1725,7 @@ describe('[Groups]', function () { }); it('/groups.invite', (done) => { - request + void request .post(api('groups.invite')) .set(credentials) .send({ @@ -1724,7 +1735,7 @@ describe('[Groups]', function () { .end(done); }); it('/groups.addModerator', (done) => { - request + void request .post(api('groups.addModerator')) .set(credentials) .send({ @@ -1734,7 +1745,7 @@ describe('[Groups]', function () { .end(done); }); it('/groups.addLeader', (done) => { - request + void request .post(api('groups.addLeader')) .set(credentials) .send({ @@ -1744,7 +1755,7 @@ describe('[Groups]', function () { .end(done); }); it('should return an array of roles <-> user relationships in a private group', (done) => { - request + void request .get(api('groups.roles')) .set(credentials) .query({ @@ -1775,7 +1786,7 @@ describe('[Groups]', function () { }); describe('/groups.moderators', () => { - let testGroup; + let testGroup: IRoom; before(async () => { await request .post(api('groups.create')) @@ -1802,7 +1813,7 @@ describe('[Groups]', function () { }); it('/groups.invite', (done) => { - request + void request .post(api('groups.invite')) .set(credentials) .send({ @@ -1812,7 +1823,7 @@ describe('[Groups]', function () { .end(done); }); it('/groups.addModerator', (done) => { - request + void request .post(api('groups.addModerator')) .set(credentials) .send({ @@ -1822,7 +1833,7 @@ describe('[Groups]', function () { .end(done); }); it('should return an array of moderators with rocket.cat as a moderator', (done) => { - request + void request .get(api('groups.moderators')) .set(credentials) .query({ @@ -1840,7 +1851,7 @@ describe('[Groups]', function () { }); describe('/groups.setEncrypted', () => { - let testGroup; + let testGroup: IRoom; before(async () => { await request @@ -1873,7 +1884,7 @@ describe('[Groups]', function () { }); it('should return an error when passing no boolean param', (done) => { - request + void request .post(api('groups.setEncrypted')) .set(credentials) .send({ @@ -1889,7 +1900,7 @@ describe('[Groups]', function () { }); it('should set group as encrypted correctly and return the new data', (done) => { - request + void request .post(api('groups.setEncrypted')) .set(credentials) .send({ @@ -1914,7 +1925,7 @@ describe('[Groups]', function () { }); it('should set group as unencrypted correctly and return the new data', (done) => { - request + void request .post(api('groups.setEncrypted')) .set(credentials) .send({ @@ -1940,7 +1951,7 @@ describe('[Groups]', function () { }); describe('/groups.convertToTeam', () => { - let newGroup; + let newGroup: IRoom; before(async () => { await request @@ -1953,12 +1964,15 @@ describe('[Groups]', function () { }); }); - after(() => Promise.all([deleteTeam(credentials, newGroup.name), updatePermission('create-team', ['admin', 'user'])])); + after(() => { + assert.isDefined(newGroup.name); + return Promise.all([deleteTeam(credentials, newGroup.name), updatePermission('create-team', ['admin', 'user'])]); + }); it('should fail to convert group if lacking edit-room permission', (done) => { - updatePermission('create-team', []).then(() => { - updatePermission('edit-room', ['admin']).then(() => { - request + void updatePermission('create-team', []).then(() => { + void updatePermission('edit-room', ['admin']).then(() => { + void request .post(api('groups.convertToTeam')) .set(credentials) .send({ roomId: newGroup._id }) @@ -1972,9 +1986,9 @@ describe('[Groups]', function () { }); it('should fail to convert group if lacking create-team permission', (done) => { - updatePermission('create-team', ['admin']).then(() => { - updatePermission('edit-room', []).then(() => { - request + void updatePermission('create-team', ['admin']).then(() => { + void updatePermission('edit-room', []).then(() => { + void request .post(api('groups.convertToTeam')) .set(credentials) .send({ roomId: newGroup._id }) @@ -1988,9 +2002,9 @@ describe('[Groups]', function () { }); it('should successfully convert a group to a team', (done) => { - updatePermission('create-team', ['admin']).then(() => { - updatePermission('edit-room', ['admin']).then(() => { - request + void updatePermission('create-team', ['admin']).then(() => { + void updatePermission('edit-room', ['admin']).then(() => { + void request .post(api('groups.convertToTeam')) .set(credentials) .send({ roomId: newGroup._id }) @@ -2004,11 +2018,11 @@ describe('[Groups]', function () { }); it('should fail to convert group without the required parameters', (done) => { - request.post(api('groups.convertToTeam')).set(credentials).send({}).expect(400).end(done); + void request.post(api('groups.convertToTeam')).set(credentials).send({}).expect(400).end(done); }); it("should fail to convert group if it's already taken", (done) => { - request + void request .post(api('groups.convertToTeam')) .set(credentials) .send({ roomId: newGroup._id }) @@ -2021,7 +2035,7 @@ describe('[Groups]', function () { }); describe("Setting: 'Use Real Name': true", () => { - let realNameGroup; + let realNameGroup: IRoom; before(async () => { await updateSetting('UI_Use_Real_Name', true); @@ -2067,7 +2081,7 @@ describe('[Groups]', function () { }); it('/groups.list', (done) => { - request + void request .get(api('groups.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2078,7 +2092,7 @@ describe('[Groups]', function () { expect(res.body).to.have.property('total'); expect(res.body).to.have.property('groups').and.to.be.an('array'); - const retGroup = res.body.groups.find(({ _id }) => _id === realNameGroup._id); + const retGroup = (res.body.groups as IRoom[]).find(({ _id }) => _id === realNameGroup._id); expect(retGroup).to.have.nested.property('lastMessage.u.name', 'RocketChat Internal Admin Test'); }) @@ -2086,7 +2100,7 @@ describe('[Groups]', function () { }); it('/groups.listAll', (done) => { - request + void request .get(api('groups.listAll')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2097,7 +2111,7 @@ describe('[Groups]', function () { expect(res.body).to.have.property('total'); expect(res.body).to.have.property('groups').and.to.be.an('array'); - const retGroup = res.body.groups.find(({ _id }) => _id === realNameGroup._id); + const retGroup = (res.body.groups as IRoom[]).find(({ _id }) => _id === realNameGroup._id); expect(retGroup).to.have.nested.property('lastMessage.u.name', 'RocketChat Internal Admin Test'); }) diff --git a/apps/meteor/tests/end-to-end/api/04-direct-message.js b/apps/meteor/tests/end-to-end/api/04-direct-message.ts similarity index 91% rename from apps/meteor/tests/end-to-end/api/04-direct-message.js rename to apps/meteor/tests/end-to-end/api/04-direct-message.ts index be8868ef6b482..b150c9f7669d6 100644 --- a/apps/meteor/tests/end-to-end/api/04-direct-message.js +++ b/apps/meteor/tests/end-to-end/api/04-direct-message.ts @@ -1,20 +1,23 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials, directMessage, apiUsername, apiEmail, methodCall } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials, apiUsername, apiEmail, methodCall } from '../../data/api-data'; import { updateSetting, updatePermission } from '../../data/permissions.helper'; import { deleteRoom } from '../../data/rooms.helper'; import { testFileUploads } from '../../data/uploads.helper'; import { password, adminUsername } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('[Direct Messages]', function () { - this.retries(0); +describe('[Direct Messages]', () => { + let directMessage: { _id: IRoom['_id'] }; before((done) => getCredentials(done)); before('/chat.postMessage', (done) => { - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -27,14 +30,14 @@ describe('[Direct Messages]', function () { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'This message was sent using the API'); expect(res.body).to.have.nested.property('message.rid'); - directMessage._id = res.body.message.rid; + directMessage = { _id: res.body.message.rid }; }) .end(done); }); describe('/im.setTopic', () => { it('should set the topic of the DM with a string', (done) => { - request + void request .post(api('im.setTopic')) .set(credentials) .send({ @@ -50,7 +53,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('should set the topic of DM with an empty string(remove the topic)', (done) => { - request + void request .post(api('im.setTopic')) .set(credentials) .send({ @@ -68,10 +71,10 @@ describe('[Direct Messages]', function () { }); describe('Testing DM info', () => { - let testDM = {}; - let dmMessage = {}; + let testDM: IRoom; + let dmMessage: IMessage; it('creating new DM...', (done) => { - request + void request .post(api('im.create')) .set(credentials) .send({ @@ -85,7 +88,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('sending a message...', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -103,7 +106,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('REACTing with last message', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -118,7 +121,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('STARring last message', (done) => { - request + void request .post(api('chat.starMessage')) .set(credentials) .send({ @@ -132,7 +135,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('PINning last message', (done) => { - request + void request .post(api('chat.pinMessage')) .set(credentials) .send({ @@ -146,7 +149,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('should return all DM messages where the last message of array should have the "star" array with USERS star ONLY', (done) => { - request + void request .get(api('im.messages')) .set(credentials) .query({ @@ -157,17 +160,17 @@ describe('[Direct Messages]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - const { messages } = res.body; + const messages = res.body.messages as IMessage[]; const lastMessage = messages.filter((message) => message._id === dmMessage._id)[0]; expect(lastMessage).to.have.property('starred').and.to.be.an('array'); - expect(lastMessage.starred[0]._id).to.be.equal(adminUsername); + expect(lastMessage.starred?.[0]._id).to.be.equal(adminUsername); }) .end(done); }); }); it('/im.history', (done) => { - request + void request .get(api('im.history')) .set(credentials) .query({ @@ -183,7 +186,7 @@ describe('[Direct Messages]', function () { }); it('/im.list', (done) => { - request + void request .get(api('im.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -207,7 +210,7 @@ describe('[Direct Messages]', function () { }); it('/im.list.everyone', (done) => { - request + void request .get(api('im.list.everyone')) .set(credentials) .expect('Content-Type', 'application/json') @@ -236,7 +239,7 @@ describe('[Direct Messages]', function () { after(async () => updateSetting('UI_Use_Real_Name', false)); it('/im.list', (done) => { - request + void request .get(api('im.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -266,7 +269,7 @@ describe('[Direct Messages]', function () { }); it('/im.list.everyone', (done) => { - request + void request .get(api('im.list.everyone')) .set(credentials) .expect('Content-Type', 'application/json') @@ -296,7 +299,7 @@ describe('[Direct Messages]', function () { }); it('/im.open', (done) => { - request + void request .post(api('im.open')) .set(credentials) .send({ @@ -311,7 +314,7 @@ describe('[Direct Messages]', function () { }); it('/im.counters', (done) => { - request + void request .get(api('im.counters')) .set(credentials) .query({ @@ -338,7 +341,7 @@ describe('[Direct Messages]', function () { describe('/im.messages', () => { it('should return all DM messages that were sent to yourself using your username', (done) => { - request + void request .get(api('im.messages')) .set(credentials) .query({ @@ -356,8 +359,8 @@ describe('[Direct Messages]', function () { describe('/im.messages.others', () => { it('should fail when the endpoint is disabled', (done) => { - updateSetting('API_Enable_Direct_Message_History_EndPoint', false).then(() => { - request + void updateSetting('API_Enable_Direct_Message_History_EndPoint', false).then(() => { + void request .get(api('im.messages.others')) .set(credentials) .query({ @@ -373,9 +376,9 @@ describe('[Direct Messages]', function () { }); }); it('should fail when the endpoint is enabled but the user doesnt have permission', (done) => { - updateSetting('API_Enable_Direct_Message_History_EndPoint', true).then(() => { - updatePermission('view-room-administration', []).then(() => { - request + void updateSetting('API_Enable_Direct_Message_History_EndPoint', true).then(() => { + void updatePermission('view-room-administration', []).then(() => { + void request .get(api('im.messages.others')) .set(credentials) .query({ @@ -392,9 +395,9 @@ describe('[Direct Messages]', function () { }); }); it('should succeed when the endpoint is enabled and user has permission', (done) => { - updateSetting('API_Enable_Direct_Message_History_EndPoint', true).then(() => { - updatePermission('view-room-administration', ['admin']).then(() => { - request + void updateSetting('API_Enable_Direct_Message_History_EndPoint', true).then(() => { + void updatePermission('view-room-administration', ['admin']).then(() => { + void request .get(api('im.messages.others')) .set(credentials) .query({ @@ -416,7 +419,7 @@ describe('[Direct Messages]', function () { }); it('/im.close', (done) => { - request + void request .post(api('im.close')) .set(credentials) .send({ @@ -436,12 +439,12 @@ describe('[Direct Messages]', function () { const name = `Name fname_${apiUsername}`; const updatedName = `Updated Name fname_${apiUsername}`; const email = `fname_${apiEmail}`; - let userId; - let directMessageId; - let user; + let userId: IUser['_id']; + let directMessageId: IMessage['_id']; + let user: TestUser; before((done) => { - request + void request .post(api('users.create')) .set(credentials) .send({ @@ -462,7 +465,7 @@ describe('[Direct Messages]', function () { }); before((done) => { - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -483,7 +486,7 @@ describe('[Direct Messages]', function () { after(async () => deleteUser(user)); it('should have fname property', (done) => { - request + void request .get(api('subscriptions.getOne')) .set(credentials) .query({ @@ -500,7 +503,7 @@ describe('[Direct Messages]', function () { }); it("should update user's name", (done) => { - request + void request .post(api('users.update')) .set(credentials) .send({ @@ -516,7 +519,7 @@ describe('[Direct Messages]', function () { }); it('should have fname property updated', (done) => { - request + void request .get(api('subscriptions.getOne')) .set(credentials) .query({ @@ -535,7 +538,7 @@ describe('[Direct Messages]', function () { describe('/im.members', () => { it('should return and array with two members', (done) => { - request + void request .get(api('im.members')) .set(credentials) .query({ @@ -553,7 +556,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('should return and array with one member', (done) => { - request + void request .get(api('im.members')) .set(credentials) .query({ @@ -571,7 +574,7 @@ describe('[Direct Messages]', function () { .end(done); }); it('should return and array with one member queried by status', (done) => { - request + void request .get(api('im.members')) .set(credentials) .query({ @@ -592,16 +595,16 @@ describe('[Direct Messages]', function () { }); describe('/im.create', () => { - let user; - let userCredentials; + let user: TestUser; + let userCredentials: Credentials; - let otherUser; - let otherUserCredentials; + let otherUser: TestUser; + let otherUserCredentials: Credentials; - let thirdUser; - let thirdUserCredentials; + let thirdUser: TestUser; + let thirdUserCredentials: Credentials; - let roomIds = {}; + let roomIds: Record = {}; // Names have to be in alfabetical order so we can test the room's fullname const userFullName = 'User A'; @@ -623,13 +626,10 @@ describe('[Direct Messages]', function () { await deleteUser(user); await deleteUser(otherUser); await deleteUser(thirdUser); - user = undefined; - otherUser = undefined; - thirdUser = undefined; }); it('creates a DM between two other parties (including self)', (done) => { - request + void request .post(api('im.create')) .set(userCredentials) .send({ @@ -647,7 +647,7 @@ describe('[Direct Messages]', function () { }); it('creates a DM between two other parties (excluding self)', (done) => { - request + void request .post(api('im.create')) .set(credentials) .send({ @@ -666,7 +666,7 @@ describe('[Direct Messages]', function () { }); it('should create a self-DM', (done) => { - request + void request .post(api('im.create')) .set(userCredentials) .send({ @@ -684,9 +684,9 @@ describe('[Direct Messages]', function () { }); describe('should create dm with correct notification preferences', () => { - let user; - let userCredentials; - let userPrefRoomId; + let user: TestUser; + let userCredentials: Credentials; + let userPrefRoomId: IRoom['_id']; before(async () => { user = await createUser(); @@ -698,7 +698,6 @@ describe('[Direct Messages]', function () { await deleteRoom({ type: 'd', roomId: userPrefRoomId }); } await deleteUser(user); - user = undefined; }); it('should save user preferences', async () => { @@ -717,7 +716,7 @@ describe('[Direct Messages]', function () { }); it('should create a DM', (done) => { - request + void request .post(api('im.create')) .set(userCredentials) .send({ @@ -735,7 +734,7 @@ describe('[Direct Messages]', function () { }); it('should return the right user notification preferences in the dm', (done) => { - request + void request .get(api('subscriptions.getOne')) .set(userCredentials) .query({ @@ -752,7 +751,7 @@ describe('[Direct Messages]', function () { }); }); - async function testRoomFNameForUser(testCredentials, roomId, fullName) { + async function testRoomFNameForUser(testCredentials: Credentials, roomId: IRoom['_id'], fullName: string) { return request .get(api('subscriptions.getOne')) .set(testCredentials) @@ -785,10 +784,10 @@ describe('[Direct Messages]', function () { }); describe('/im.delete', () => { - let testDM; + let testDM: IRoom; it('/im.create', (done) => { - request + void request .post(api('im.create')) .set(credentials) .send({ @@ -803,7 +802,7 @@ describe('[Direct Messages]', function () { }); it('/im.delete', (done) => { - request + void request .post(api('im.delete')) .set(credentials) .send({ @@ -818,7 +817,7 @@ describe('[Direct Messages]', function () { }); it('/im.open', (done) => { - request + void request .post(api('im.open')) .set(credentials) .send({ @@ -834,8 +833,8 @@ describe('[Direct Messages]', function () { }); describe('when authenticated as a non-admin user', () => { - let otherUser; - let otherCredentials; + let otherUser: TestUser; + let otherCredentials: Credentials; before(async () => { otherUser = await createUser(); @@ -844,11 +843,10 @@ describe('[Direct Messages]', function () { after(async () => { await deleteUser(otherUser); - otherUser = undefined; }); it('/im.create', (done) => { - request + void request .post(api('im.create')) .set(credentials) .send({ @@ -863,7 +861,7 @@ describe('[Direct Messages]', function () { }); it('/im.delete', (done) => { - request + void request .post(api('im.delete')) .set(otherCredentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.ts similarity index 92% rename from apps/meteor/tests/end-to-end/api/05-chat.js rename to apps/meteor/tests/end-to-end/api/05-chat.ts index 96aa276e1a9c3..6e2f004b47f0b 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.ts @@ -1,17 +1,31 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IMessage, IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, beforeEach, describe, it } from 'mocha'; +import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials, message } from '../../data/api-data.js'; -import { sendSimpleMessage, deleteMessage, pinMessage } from '../../data/chat.helper.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('[Chat]', function () { - this.retries(0); - let testChannel; +const pinMessage = ({ msgId }: { msgId: IMessage['_id'] }) => { + if (!msgId) { + throw new Error('"msgId" is required in "pinMessage" test helper'); + } + + return request.post(api('chat.pinMessage')).set(credentials).send({ + messageId: msgId, + }); +}; + +describe('[Chat]', () => { + let testChannel: IRoom; + let message: { _id: IMessage['_id'] }; before((done) => getCredentials(done)); @@ -23,7 +37,7 @@ describe('[Chat]', function () { describe('/chat.postMessage', () => { it('should throw an error when at least one of required parameters(channel, roomId) is not sent', (done) => { - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -42,7 +56,7 @@ describe('[Chat]', function () { }); it('should throw an error when it has some properties with the wrong type(attachments.title_link_download, attachments.fields, message_link)', (done) => { - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -83,7 +97,7 @@ describe('[Chat]', function () { describe('should throw an error when the sensitive properties contain malicious XSS values', () => { it('attachment.message_link', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -110,7 +124,7 @@ describe('[Chat]', function () { })); it('attachment.author_link', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -137,7 +151,7 @@ describe('[Chat]', function () { })); it('attachment.title_link', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -164,7 +178,7 @@ describe('[Chat]', function () { })); it('attachment.action.url', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -198,7 +212,7 @@ describe('[Chat]', function () { })); it('message.avatar', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -232,7 +246,7 @@ describe('[Chat]', function () { })); it('attachment.action.image_url', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -267,7 +281,7 @@ describe('[Chat]', function () { })); it('attachment.thumb_url', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -295,7 +309,7 @@ describe('[Chat]', function () { })); it('attachment.author_icon', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -323,7 +337,7 @@ describe('[Chat]', function () { })); it('attachment.image_url', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -350,7 +364,7 @@ describe('[Chat]', function () { expect(res.body).to.have.property('error'); })); it('attachment.audio_url', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -377,7 +391,7 @@ describe('[Chat]', function () { expect(res.body).to.have.property('error'); })); it('attachment.video_url', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -406,7 +420,7 @@ describe('[Chat]', function () { }); it('should throw an error when the properties (attachments.fields.title, attachments.fields.value) are with the wrong type', (done) => { - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -452,7 +466,7 @@ describe('[Chat]', function () { }); it('should return statusCode 200 when postMessage successfully', (done) => { - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -496,7 +510,7 @@ describe('[Chat]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message.msg', 'Sample message'); - message._id = res.body.message._id; + message = { _id: res.body.message._id }; }) .end(done); }); @@ -504,7 +518,7 @@ describe('[Chat]', function () { describe('/chat.getMessage', () => { it('should retrieve the message successfully', (done) => { - request + void request .get(api('chat.getMessage')) .set(credentials) .query({ @@ -522,7 +536,7 @@ describe('[Chat]', function () { describe('/chat.sendMessage', () => { it("should throw an error when the required param 'rid' is not sent", (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -544,7 +558,7 @@ describe('[Chat]', function () { describe('should throw an error when the sensitive properties contain malicious XSS values', () => { it('attachment.message_link', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -571,7 +585,7 @@ describe('[Chat]', function () { })); it('attachment.author_link', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -598,7 +612,7 @@ describe('[Chat]', function () { })); it('attachment.title_link', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -625,7 +639,7 @@ describe('[Chat]', function () { })); it('attachment.action.url', () => - request + void request .post(api('chat.postMessage')) .set(credentials) .send({ @@ -660,7 +674,7 @@ describe('[Chat]', function () { }); it('should throw an error when it has some properties with the wrong type(attachments.title_link_download, attachments.fields, message_link)', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -703,7 +717,7 @@ describe('[Chat]', function () { it('should send a message successfully', (done) => { message._id = `id-${Date.now()}`; - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -755,8 +769,8 @@ describe('[Chat]', function () { }); describe('oembed', () => { - let ytEmbedMsgId; - let imgUrlMsgId; + let ytEmbedMsgId: IMessage['_id']; + let imgUrlMsgId: IMessage['_id']; before(() => Promise.all([updateSetting('API_EmbedIgnoredHosts', ''), updateSetting('API_EmbedSafePorts', '80, 443, 3000')])); @@ -793,7 +807,7 @@ describe('[Chat]', function () { it('should have an iframe oembed with style max-width', (done) => { setTimeout(() => { - request + void request .get(api('chat.getMessage')) .set(credentials) .query({ @@ -815,7 +829,7 @@ describe('[Chat]', function () { it('should embed an image preview if message has an image url', (done) => { setTimeout(() => { - request + void request .get(api('chat.getMessage')) .set(credentials) .query({ @@ -945,7 +959,7 @@ describe('[Chat]', function () { .expect((res) => { expect(res.body).to.have.property('message').to.have.property('urls').to.be.an('array').that.has.lengthOf(urls.length); - res.body.message.urls.forEach((url) => { + (res.body.message as IMessage).urls?.forEach((url) => { expect(url).to.not.have.property('ignoreParse'); expect(url).to.have.property('meta').that.is.an('object').that.is.empty; }); @@ -954,9 +968,9 @@ describe('[Chat]', function () { }); describe('Read only channel', () => { - let readOnlyChannel; - let userCredentials; - let user; + let readOnlyChannel: IRoom; + let userCredentials: Credentials; + let user: TestUser; before(async () => { user = await createUser(); @@ -972,7 +986,7 @@ describe('[Chat]', function () { ); it('Creating a read-only channel', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -988,7 +1002,7 @@ describe('[Chat]', function () { .end(done); }); it('should send a message when the user is the owner of a readonly channel', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1006,7 +1020,7 @@ describe('[Chat]', function () { .end(done); }); it('Inviting regular user to read-only channel', (done) => { - request + void request .post(api('channels.invite')) .set(credentials) .send({ @@ -1024,7 +1038,7 @@ describe('[Chat]', function () { }); it('should fail to send message when the user lacks permission', (done) => { - request + void request .post(api('chat.sendMessage')) .set(userCredentials) .send({ @@ -1064,7 +1078,7 @@ describe('[Chat]', function () { }); it('should fail if user does not have the message-impersonate permission and tries to send message with alias param', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1084,7 +1098,7 @@ describe('[Chat]', function () { }); it('should fail if user does not have the message-impersonate permission and tries to send message with avatar param', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1104,7 +1118,15 @@ describe('[Chat]', function () { }); describe('customFields', () => { - async function testMessageSending({ customFields, testCb, statusCode }) { + async function testMessageSending({ + customFields, + testCb, + statusCode, + }: { + customFields?: Record; + testCb: (res: Response) => any; + statusCode: number; + }) { await request .post(api('chat.sendMessage')) .set(credentials) @@ -1334,7 +1356,7 @@ describe('[Chat]', function () { describe('/chat.update', () => { const siteUrl = process.env.SITE_URL || process.env.TEST_API_URL || 'http://localhost:3000'; - let simpleMessageId; + let simpleMessageId: IMessage['_id']; before('should send simple message in room', async () => { const res = await sendSimpleMessage({ roomId: 'GENERAL' }); @@ -1342,7 +1364,7 @@ describe('[Chat]', function () { }); it('should update a message successfully', (done) => { - request + void request .post(api('chat.update')) .set(credentials) .send({ @@ -1361,7 +1383,7 @@ describe('[Chat]', function () { it('should add quote attachments to a message', async () => { const quotedMsgLink = `${siteUrl}/channel/general?msg=${message._id}`; - request + void request .post(api('chat.update')) .set(credentials) .send({ @@ -1381,7 +1403,7 @@ describe('[Chat]', function () { it('should replace a quote attachment in a message', async () => { const quotedMsgLink = `${siteUrl}/channel/general?msg=${simpleMessageId}`; - request + void request .post(api('chat.update')) .set(credentials) .send({ @@ -1402,7 +1424,7 @@ describe('[Chat]', function () { it('should add multiple quote attachments in a single message', async () => { const quotedMsgLink = `${siteUrl}/channel/general?msg=${simpleMessageId}`; const newQuotedMsgLink = `${siteUrl}/channel/general?msg=${message._id}`; - request + void request .post(api('chat.update')) .set(credentials) .send({ @@ -1441,9 +1463,9 @@ describe('[Chat]', function () { }); describe('[/chat.delete]', () => { - let msgId; - let user; - let userCredentials; + let msgId: IMessage['_id']; + let user: TestUser; + let userCredentials: Credentials; before(async () => { user = await createUser(); @@ -1453,7 +1475,7 @@ describe('[Chat]', function () { after(() => deleteUser(user)); beforeEach((done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1471,7 +1493,7 @@ describe('[Chat]', function () { .end(done); }); it('should delete a message successfully', (done) => { - request + void request .post(api('chat.delete')) .set(credentials) .send({ @@ -1486,7 +1508,7 @@ describe('[Chat]', function () { .end(done); }); it('sending message as another user...', (done) => { - request + void request .post(api('chat.sendMessage')) .set(userCredentials) .send({ @@ -1504,7 +1526,7 @@ describe('[Chat]', function () { .end(done); }); it('should delete a message successfully when the user deletes a message send by another user', (done) => { - request + void request .post(api('chat.delete')) .set(credentials) .send({ @@ -1523,7 +1545,7 @@ describe('[Chat]', function () { describe('/chat.search', () => { before(async () => { - const sendMessage = (text) => + const sendMessage = (text: string) => request .post(api('chat.sendMessage')) .set(credentials) @@ -1542,7 +1564,7 @@ describe('[Chat]', function () { }); it('should return a list of messages when execute successfully', (done) => { - request + void request .get(api('chat.search')) .set(credentials) .query({ @@ -1558,7 +1580,7 @@ describe('[Chat]', function () { .end(done); }); it('should return a list of messages(length=1) when is provided "count" query parameter execute successfully', (done) => { - request + void request .get(api('chat.search')) .set(credentials) .query({ @@ -1576,7 +1598,7 @@ describe('[Chat]', function () { .end(done); }); it('should return a list of messages(length=3) when is provided "count" and "offset" query parameters are executed successfully', (done) => { - request + void request .get(api('chat.search')) .set(credentials) .query({ @@ -1596,7 +1618,7 @@ describe('[Chat]', function () { }); it('should return a empty list of messages when is provided a huge offset value', (done) => { - request + void request .get(api('chat.search')) .set(credentials) .query({ @@ -1618,7 +1640,7 @@ describe('[Chat]', function () { describe('[/chat.react]', () => { it("should return statusCode: 200 and success when try unreact a message that's no reacted yet", (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1634,7 +1656,7 @@ describe('[Chat]', function () { .end(done); }); it('should react a message successfully', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1650,7 +1672,7 @@ describe('[Chat]', function () { }); it('should return statusCode: 200 when the emoji is valid', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1665,7 +1687,7 @@ describe('[Chat]', function () { .end(done); }); it("should return statusCode: 200 and success when try react a message that's already reacted", (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1681,7 +1703,7 @@ describe('[Chat]', function () { .end(done); }); it('should return statusCode: 200 when unreact a message with flag, shouldReact: false', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1697,7 +1719,7 @@ describe('[Chat]', function () { .end(done); }); it('should return statusCode: 200 when react a message with flag, shouldReact: true', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1713,7 +1735,7 @@ describe('[Chat]', function () { .end(done); }); it('should return statusCode: 200 when the emoji is valid and has no colons', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1728,7 +1750,7 @@ describe('[Chat]', function () { .end(done); }); it('should return statusCode: 200 for reaction property when the emoji is valid', (done) => { - request + void request .post(api('chat.react')) .set(credentials) .send({ @@ -1752,7 +1774,7 @@ describe('[Chat]', function () { this.skip(); } - request + void request .get(api(`chat.getMessageReadReceipts`)) .set(credentials) .query({ @@ -1775,7 +1797,7 @@ describe('[Chat]', function () { if (isEnterprise) { this.skip(); } - request + void request .get(api(`chat.getMessageReadReceipts`)) .set(credentials) .query({ @@ -1796,7 +1818,7 @@ describe('[Chat]', function () { this.skip(); } - request + void request .get(api('chat.getMessageReadReceipts')) .set(credentials) .expect('Content-Type', 'application/json') @@ -1814,7 +1836,7 @@ describe('[Chat]', function () { describe('[/chat.reportMessage]', () => { describe('when execute successfully', () => { it('should return the statusCode 200', (done) => { - request + void request .post(api('chat.reportMessage')) .set(credentials) .send({ @@ -1832,7 +1854,7 @@ describe('[Chat]', function () { describe('when an error occurs', () => { it('should return statusCode 400 and an error', (done) => { - request + void request .post(api('chat.reportMessage')) .set(credentials) .send({ @@ -1850,7 +1872,7 @@ describe('[Chat]', function () { }); describe('[/chat.getDeletedMessages]', () => { - let roomId; + let roomId: IRoom['_id']; before(async () => { roomId = ( @@ -1867,7 +1889,7 @@ describe('[Chat]', function () { describe('when execute successfully', () => { it('should return a list of deleted messages', (done) => { - request + void request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ @@ -1884,7 +1906,7 @@ describe('[Chat]', function () { .end(done); }); it('should return a list of deleted messages when the user sets count query parameter', (done) => { - request + void request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ @@ -1902,7 +1924,7 @@ describe('[Chat]', function () { .end(done); }); it('should return a list of deleted messages when the user sets count and offset query parameters', (done) => { - request + void request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ @@ -1924,7 +1946,7 @@ describe('[Chat]', function () { describe('when an error occurs', () => { it('should return statusCode 400 and an error when "roomId" is not provided', (done) => { - request + void request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ @@ -1941,7 +1963,7 @@ describe('[Chat]', function () { .end(done); }); it('should return statusCode 400 and an error when "since" is not provided', (done) => { - request + void request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ @@ -1958,7 +1980,7 @@ describe('[Chat]', function () { .end(done); }); it('should return statusCode 400 and an error when "since" is provided but it is invalid ISODate', (done) => { - request + void request .get(api('chat.getDeletedMessages')) .set(credentials) .query({ @@ -1984,8 +2006,8 @@ describe('[Chat]', function () { ); it('should return an error when pinMessage is not allowed in this server', (done) => { - updateSetting('Message_AllowPinning', false).then(() => { - request + void updateSetting('Message_AllowPinning', false).then(() => { + void request .post(api('chat.pinMessage')) .set(credentials) .send({ @@ -2002,9 +2024,9 @@ describe('[Chat]', function () { }); it('should return an error when pinMessage is allowed in server but user dont have permission', (done) => { - updateSetting('Message_AllowPinning', true).then(() => { - updatePermission('pin-message', []).then(() => { - request + void updateSetting('Message_AllowPinning', true).then(() => { + void updatePermission('pin-message', []).then(() => { + void request .post(api('chat.pinMessage')) .set(credentials) .send({ @@ -2022,8 +2044,8 @@ describe('[Chat]', function () { }); it('should pin Message successfully', (done) => { - updatePermission('pin-message', ['admin']).then(() => { - request + void updatePermission('pin-message', ['admin']).then(() => { + void request .post(api('chat.pinMessage')) .set(credentials) .send({ @@ -2046,8 +2068,8 @@ describe('[Chat]', function () { ); it('should return an error when pinMessage is not allowed in this server', (done) => { - updateSetting('Message_AllowPinning', false).then(() => { - request + void updateSetting('Message_AllowPinning', false).then(() => { + void request .post(api('chat.unPinMessage')) .set(credentials) .send({ @@ -2064,9 +2086,9 @@ describe('[Chat]', function () { }); it('should return an error when pinMessage is allowed in server but users dont have permission', (done) => { - updateSetting('Message_AllowPinning', true).then(() => { - updatePermission('pin-message', []).then(() => { - request + void updateSetting('Message_AllowPinning', true).then(() => { + void updatePermission('pin-message', []).then(() => { + void request .post(api('chat.unPinMessage')) .set(credentials) .send({ @@ -2084,8 +2106,8 @@ describe('[Chat]', function () { }); it('should unpin Message successfully', (done) => { - updatePermission('pin-message', ['admin']).then(() => { - request + void updatePermission('pin-message', ['admin']).then(() => { + void request .post(api('chat.unPinMessage')) .set(credentials) .send({ @@ -2106,8 +2128,8 @@ describe('[Chat]', function () { after(() => updateSetting('Message_AllowStarring', true)); it('should return an error when starMessage is not allowed in this server', (done) => { - updateSetting('Message_AllowStarring', false).then(() => { - request + void updateSetting('Message_AllowStarring', false).then(() => { + void request .post(api('chat.unStarMessage')) .set(credentials) .send({ @@ -2124,8 +2146,8 @@ describe('[Chat]', function () { }); it('should unstar Message successfully', (done) => { - updateSetting('Message_AllowStarring', true).then(() => { - request + void updateSetting('Message_AllowStarring', true).then(() => { + void request .post(api('chat.unStarMessage')) .set(credentials) .send({ @@ -2146,8 +2168,8 @@ describe('[Chat]', function () { after(() => updateSetting('Message_AllowStarring', true)); it('should return an error when starMessage is not allowed in this server', (done) => { - updateSetting('Message_AllowStarring', false).then(() => { - request + void updateSetting('Message_AllowStarring', false).then(() => { + void request .post(api('chat.starMessage')) .set(credentials) .send({ @@ -2164,8 +2186,8 @@ describe('[Chat]', function () { }); it('should star Message successfully', (done) => { - updateSetting('Message_AllowStarring', true).then(() => { - request + void updateSetting('Message_AllowStarring', true).then(() => { + void request .post(api('chat.starMessage')) .set(credentials) .send({ @@ -2186,7 +2208,7 @@ describe('[Chat]', function () { after(() => deleteRoom({ type: 'd', roomId: 'rocket.catrocketchat.internal.admin.test' })); it('should fail if invalid roomId', (done) => { - request + void request .get(api('chat.ignoreUser')) .set(credentials) .query({ @@ -2204,7 +2226,7 @@ describe('[Chat]', function () { }); }); it('should fail if invalid userId', (done) => { - request + void request .get(api('chat.ignoreUser')) .set(credentials) .query({ @@ -2222,7 +2244,7 @@ describe('[Chat]', function () { }); }); it('should successfully ignore user', (done) => { - request + void request .get(api('chat.ignoreUser')) .set(credentials) .query({ @@ -2239,7 +2261,7 @@ describe('[Chat]', function () { }); }); it('should successfully unignore user', (done) => { - request + void request .get(api('chat.ignoreUser')) .set(credentials) .query({ @@ -2259,7 +2281,7 @@ describe('[Chat]', function () { }); describe('[/chat.getPinnedMessages]', () => { - let roomId; + let roomId: IRoom['_id']; before(async () => { roomId = ( @@ -2278,7 +2300,7 @@ describe('[Chat]', function () { describe('when execute successfully', () => { it('should return a list of pinned messages', (done) => { - request + void request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ @@ -2294,7 +2316,7 @@ describe('[Chat]', function () { .end(done); }); it('should return a list of pinned messages when the user sets count query parameter', (done) => { - request + void request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ @@ -2311,7 +2333,7 @@ describe('[Chat]', function () { .end(done); }); it('should return a list of pinned messages when the user sets count and offset query parameters', (done) => { - request + void request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ @@ -2332,7 +2354,7 @@ describe('[Chat]', function () { describe('when an error occurs', () => { it('should return statusCode 400 and an error when "roomId" is not provided', (done) => { - request + void request .get(api('chat.getPinnedMessages')) .set(credentials) .query({ @@ -2351,7 +2373,7 @@ describe('[Chat]', function () { }); describe('[/chat.getMentionedMessages]', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = ( @@ -2365,7 +2387,7 @@ describe('[Chat]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should return an error when the required "roomId" parameter is not sent', (done) => { - request + void request .get(api('chat.getMentionedMessages')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2378,8 +2400,9 @@ describe('[Chat]', function () { }); it('should return an error when the roomId is invalid', (done) => { - request - .get(api('chat.getMentionedMessages?roomId=invalid-room')) + void request + .get(api('chat.getMentionedMessages')) + .query({ roomId: 'invalid-room' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -2391,8 +2414,9 @@ describe('[Chat]', function () { }); it('should return the mentioned messages', (done) => { - request - .get(api(`chat.getMentionedMessages?roomId=${testChannel._id}`)) + void request + .get(api('chat.getMentionedMessages')) + .query({ roomId: testChannel._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2408,7 +2432,7 @@ describe('[Chat]', function () { }); describe('[/chat.getStarredMessages]', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = ( @@ -2422,7 +2446,7 @@ describe('[Chat]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should return an error when the required "roomId" parameter is not sent', (done) => { - request + void request .get(api('chat.getStarredMessages')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2435,8 +2459,9 @@ describe('[Chat]', function () { }); it('should return an error when the roomId is invalid', (done) => { - request - .get(api('chat.getStarredMessages?roomId=invalid-room')) + void request + .get(api('chat.getStarredMessages')) + .query({ roomId: 'invalid-room' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -2448,8 +2473,9 @@ describe('[Chat]', function () { }); it('should return the starred messages', (done) => { - request - .get(api(`chat.getStarredMessages?roomId=${testChannel._id}`)) + void request + .get(api('chat.getStarredMessages')) + .query({ roomId: testChannel._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2466,8 +2492,8 @@ describe('[Chat]', function () { describe('[/chat.getDiscussions]', () => { const messageText = 'Message to create discussion'; - let testChannel; - let discussionRoom; + let testChannel: IRoom; + let discussionRoom: IRoom & { rid: IRoom['_id'] }; const messageWords = [ ...messageText.split(' '), ...messageText.toUpperCase().split(' '), @@ -2496,7 +2522,7 @@ describe('[Chat]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should return an error when the required "roomId" parameter is not sent', (done) => { - request + void request .get(api('chat.getDiscussions')) .set(credentials) .expect('Content-Type', 'application/json') @@ -2509,8 +2535,9 @@ describe('[Chat]', function () { }); it('should return an error when the roomId is invalid', (done) => { - request - .get(api('chat.getDiscussions?roomId=invalid-room')) + void request + .get(api('chat.getDiscussions')) + .query({ roomId: 'invalid-room' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -2522,8 +2549,9 @@ describe('[Chat]', function () { }); it('should return the discussions of a room', (done) => { - request - .get(api(`chat.getDiscussions?roomId=${testChannel._id}`)) + void request + .get(api('chat.getDiscussions')) + .query({ roomId: testChannel._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2537,7 +2565,7 @@ describe('[Chat]', function () { .end(done); }); it('should return the discussions of a room even requested with count and offset params', (done) => { - request + void request .get(api('chat.getDiscussions')) .set(credentials) .query({ @@ -2557,9 +2585,9 @@ describe('[Chat]', function () { .end(done); }); - function filterDiscussionsByText(text) { + function filterDiscussionsByText(text: string) { it(`should return the room's discussion list filtered by the text '${text}'`, (done) => { - request + void request .get(api('chat.getDiscussions')) .set(credentials) .query({ @@ -2581,7 +2609,7 @@ describe('[Chat]', function () { }); it(`should return the room's discussion list filtered by the text '${text}' even requested with count and offset params`, (done) => { - request + void request .get(api('chat.getDiscussions')) .set(credentials) .query({ @@ -2612,7 +2640,7 @@ describe('[Chat]', function () { }); describe('Threads', () => { - let testThreadChannel; + let testThreadChannel: IRoom; before((done) => getCredentials(done)); @@ -2632,9 +2660,9 @@ describe('Threads', () => { describe('[/chat.getThreadsList]', () => { const messageText = 'Message to create thread'; - let testChannel; - let threadMessage; - let user; + let testChannel: IRoom; + let threadMessage: IThreadMessage; + let user: TestUser; const messageWords = [ ...messageText.split(' '), ...messageText.toUpperCase().split(' '), @@ -2686,8 +2714,8 @@ describe('Threads', () => { ); it('should return an error for chat.getThreadsList when threads are not allowed in this server', (done) => { - updateSetting('Threads_enabled', false).then(() => { - request + void updateSetting('Threads_enabled', false).then(() => { + void request .get(api('chat.getThreadsList')) .set(credentials) .query({ @@ -2705,12 +2733,12 @@ describe('Threads', () => { }); it('should return an error when the user is not allowed access the room', (done) => { - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(createdUser.username, password).then((userCredentials) => { - updateSetting('Threads_enabled', true).then(() => { - updatePermission('view-c-room', []).then(() => { - request + void login(createdUser.username, password).then((userCredentials) => { + void updateSetting('Threads_enabled', true).then(() => { + void updatePermission('view-c-room', []).then(() => { + void request .get(api('chat.getThreadsList')) .set(userCredentials) .query({ @@ -2731,8 +2759,8 @@ describe('Threads', () => { }); it("should return the room's thread list", (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .get(api('chat.getThreadsList')) .set(credentials) .query({ @@ -2754,8 +2782,8 @@ describe('Threads', () => { }); it("should return the room's thread list even requested with count and offset params", (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .get(api('chat.getThreadsList')) .set(credentials) .query({ @@ -2778,9 +2806,9 @@ describe('Threads', () => { }); }); - function filterThreadsByText(text) { + function filterThreadsByText(text: string) { it(`should return the room's thread list filtered by the text '${text}'`, (done) => { - request + void request .get(api('chat.getThreadsList')) .set(credentials) .query({ @@ -2801,7 +2829,7 @@ describe('Threads', () => { .end(done); }); it(`should return the room's thread list filtered by the text '${text}' even requested with count and offset params`, (done) => { - request + void request .get(api('chat.getThreadsList')) .set(credentials) .query({ @@ -2830,8 +2858,8 @@ describe('Threads', () => { }); it('should return an empty thread list', (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .get(api('chat.getThreadsList')) .set(credentials) .query({ @@ -2854,9 +2882,9 @@ describe('Threads', () => { }); describe('[/chat.syncThreadsList]', () => { - let testChannel; - let threadMessage; - let user; + let testChannel: IRoom; + let threadMessage: IThreadMessage; + let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `.threads.sync.${Date.now()}` })).body.channel; @@ -2884,8 +2912,8 @@ describe('Threads', () => { ); it('should return an error for chat.getThreadsList when threads are not allowed in this server', (done) => { - updateSetting('Threads_enabled', false).then(() => { - request + void updateSetting('Threads_enabled', false).then(() => { + void request .get(api('chat.getThreadsList')) .set(credentials) .query({ @@ -2903,8 +2931,8 @@ describe('Threads', () => { }); it('should return an error when the required param "rid" is missing', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .get(api('chat.syncThreadsList')) .set(credentials) .query({}) @@ -2920,8 +2948,8 @@ describe('Threads', () => { }); it('should return an error when the required param "updatedSince" is missing', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .get(api('chat.syncThreadsList')) .set(credentials) .query({ @@ -2939,8 +2967,8 @@ describe('Threads', () => { }); it('should return an error when the param "updatedSince" is an invalid date', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .get(api('chat.syncThreadsList')) .set(credentials) .query({ @@ -2962,11 +2990,11 @@ describe('Threads', () => { }); it('should return an error when the user is not allowed access the room', (done) => { - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(createdUser.username, password).then((userCredentials) => { - updatePermission('view-c-room', []).then(() => { - request + void login(createdUser.username, password).then((userCredentials) => { + void updatePermission('view-c-room', []).then(() => { + void request .get(api('chat.syncThreadsList')) .set(userCredentials) .query({ @@ -2987,8 +3015,8 @@ describe('Threads', () => { }); it("should return the room's thread synced list", (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .get(api('chat.syncThreadsList')) .set(credentials) .query({ @@ -3012,10 +3040,10 @@ describe('Threads', () => { }); describe('[/chat.getThreadMessages]', () => { - let testChannel; - let threadMessage; - let createdThreadMessage; - let user; + let testChannel: IRoom; + let threadMessage: IThreadMessage; + let createdThreadMessage: IThreadMessage; + let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.threads.${Date.now()}` })).body.channel; @@ -3045,8 +3073,8 @@ describe('Threads', () => { ); it('should return an error for chat.getThreadMessages when threads are not allowed in this server', (done) => { - updateSetting('Threads_enabled', false).then(() => { - request + void updateSetting('Threads_enabled', false).then(() => { + void request .get(api('chat.getThreadMessages')) .set(credentials) .query({ @@ -3064,12 +3092,12 @@ describe('Threads', () => { }); it('should return an error when the user is not allowed access the room', (done) => { - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(createdUser.username, password).then((userCredentials) => { - updateSetting('Threads_enabled', true).then(() => { - updatePermission('view-c-room', []).then(() => { - request + void login(createdUser.username, password).then((userCredentials) => { + void updateSetting('Threads_enabled', true).then(() => { + void updatePermission('view-c-room', []).then(() => { + void request .get(api('chat.getThreadMessages')) .set(userCredentials) .query({ @@ -3090,8 +3118,8 @@ describe('Threads', () => { }); it("should return the thread's message list", (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .get(api('chat.getThreadMessages')) .set(credentials) .query({ @@ -3114,10 +3142,10 @@ describe('Threads', () => { }); describe('[/chat.syncThreadMessages]', () => { - let testChannel; - let threadMessage; - let createdThreadMessage; - let user; + let testChannel: IRoom; + let threadMessage: IThreadMessage; + let createdThreadMessage: IThreadMessage; + let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `message.threads.${Date.now()}` })).body.channel; @@ -3147,8 +3175,8 @@ describe('Threads', () => { ); it('should return an error for chat.syncThreadMessages when threads are not allowed in this server', (done) => { - updateSetting('Threads_enabled', false).then(() => { - request + void updateSetting('Threads_enabled', false).then(() => { + void request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ @@ -3167,8 +3195,8 @@ describe('Threads', () => { }); it('should return an error when the required param "tmid" is missing', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .get(api('chat.syncThreadMessages')) .set(credentials) .query({}) @@ -3184,8 +3212,8 @@ describe('Threads', () => { }); it('should return an error when the required param "updatedSince" is missing', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ @@ -3203,8 +3231,8 @@ describe('Threads', () => { }); it('should return an error when the param "updatedSince" is an invalid date', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ @@ -3226,11 +3254,11 @@ describe('Threads', () => { }); it('should return an error when the user is not allowed access the room', (done) => { - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(createdUser.username, password).then((userCredentials) => { - updatePermission('view-c-room', []).then(() => { - request + void login(createdUser.username, password).then((userCredentials) => { + void updatePermission('view-c-room', []).then(() => { + void request .get(api('chat.syncThreadMessages')) .set(userCredentials) .query({ @@ -3251,8 +3279,8 @@ describe('Threads', () => { }); it("should return the thread's message list", (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .get(api('chat.syncThreadMessages')) .set(credentials) .query({ @@ -3276,9 +3304,9 @@ describe('Threads', () => { }); describe('[/chat.followMessage]', () => { - let testChannel; - let threadMessage; - let user; + let testChannel: IRoom; + let threadMessage: IThreadMessage; + let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.threads.follow${Date.now()}` })).body.channel; @@ -3306,8 +3334,8 @@ describe('Threads', () => { ); it('should return an error for chat.followMessage when threads are not allowed in this server', (done) => { - updateSetting('Threads_enabled', false).then(() => { - request + void updateSetting('Threads_enabled', false).then(() => { + void request .post(api('chat.followMessage')) .set(credentials) .send({ @@ -3325,8 +3353,8 @@ describe('Threads', () => { }); it('should return an error when the message does not exist', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .post(api('chat.followMessage')) .set(credentials) .send({ @@ -3344,11 +3372,11 @@ describe('Threads', () => { }); it('should return an error when the user is not allowed access the room', (done) => { - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(createdUser.username, password).then((userCredentials) => { - updatePermission('view-c-room', []).then(() => { - request + void login(createdUser.username, password).then((userCredentials) => { + void updatePermission('view-c-room', []).then(() => { + void request .post(api('chat.followMessage')) .set(userCredentials) .send({ @@ -3368,8 +3396,8 @@ describe('Threads', () => { }); it('should return success: true when it execute successfully', (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .post(api('chat.followMessage')) .set(credentials) .send({ @@ -3386,9 +3414,9 @@ describe('Threads', () => { }); describe('[/chat.unfollowMessage]', () => { - let testChannel; - let threadMessage; - let user; + let testChannel: IRoom; + let threadMessage: IThreadMessage; + let user: TestUser; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.threads.unfollow.${Date.now()}` })).body.channel; @@ -3415,8 +3443,8 @@ describe('Threads', () => { ]), ); it('should return an error for chat.unfollowMessage when threads are not allowed in this server', (done) => { - updateSetting('Threads_enabled', false).then(() => { - request + void updateSetting('Threads_enabled', false).then(() => { + void request .post(api('chat.unfollowMessage')) .set(credentials) .send({ @@ -3434,8 +3462,8 @@ describe('Threads', () => { }); it('should return an error when the message does not exist', (done) => { - updateSetting('Threads_enabled', true).then(() => { - request + void updateSetting('Threads_enabled', true).then(() => { + void request .post(api('chat.unfollowMessage')) .set(credentials) .send({ @@ -3453,11 +3481,11 @@ describe('Threads', () => { }); it('should return an error when the user is not allowed access the room', (done) => { - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(createdUser.username, password).then((userCredentials) => { - updatePermission('view-c-room', []).then(() => { - request + void login(createdUser.username, password).then((userCredentials) => { + void updatePermission('view-c-room', []).then(() => { + void request .post(api('chat.unfollowMessage')) .set(userCredentials) .send({ @@ -3477,8 +3505,8 @@ describe('Threads', () => { }); it('should return success: true when it execute successfully', (done) => { - updatePermission('view-c-room', ['admin', 'user']).then(() => { - request + void updatePermission('view-c-room', ['admin', 'user']).then(() => { + void request .post(api('chat.unfollowMessage')) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/06-outgoing-integrations.js b/apps/meteor/tests/end-to-end/api/06-outgoing-integrations.ts similarity index 82% rename from apps/meteor/tests/end-to-end/api/06-outgoing-integrations.js rename to apps/meteor/tests/end-to-end/api/06-outgoing-integrations.ts index 120b467ec1b85..5ed7f8604db11 100644 --- a/apps/meteor/tests/end-to-end/api/06-outgoing-integrations.js +++ b/apps/meteor/tests/end-to-end/api/06-outgoing-integrations.ts @@ -1,19 +1,19 @@ -import { expect } from 'chai'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IIntegration, IUser } from '@rocket.chat/core-typings'; +import { assert, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission } from '../../data/permissions.helper'; import { password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('[Outgoing Integrations]', function () { - this.retries(0); - - let integration; - let integrationCreatedByAnUser; - let user; - let userCredentials; +describe('[Outgoing Integrations]', () => { + let integrationCreatedByAnUser: IIntegration; + let user: TestUser; + let userCredentials: Credentials; before((done) => getCredentials(done)); @@ -59,10 +59,12 @@ describe('[Outgoing Integrations]', function () { ]), ); + let integration: IIntegration; + describe('[/integrations.create]', () => { it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to add an outgoing integration', (done) => { - updatePermission('manage-outgoing-integrations', []).then(() => { - request + void updatePermission('manage-outgoing-integrations', []).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -90,8 +92,8 @@ describe('[Outgoing Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-own-outgoing-integrations" to add an outgoing integration', (done) => { - updatePermission('manage-own-outgoing-integrations', []).then(() => { - request + void updatePermission('manage-own-outgoing-integrations', []).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -119,7 +121,7 @@ describe('[Outgoing Integrations]', function () { }); it('should return an error when the user sends an invalid type of integration', (done) => { - request + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -146,9 +148,9 @@ describe('[Outgoing Integrations]', function () { }); it('should add the integration successfully when the user ONLY has the permission "manage-outgoing-integrations" to add an outgoing integration', (done) => { - let integrationId; - updatePermission('manage-outgoing-integrations', ['admin']).then(() => { - request + let integrationId: IIntegration['_id']; + void updatePermission('manage-outgoing-integrations', ['admin']).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -177,9 +179,9 @@ describe('[Outgoing Integrations]', function () { }); it('should add the integration successfully when the user ONLY has the permission "manage-own-outgoing-integrations" to add an outgoing integration', (done) => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-own-outgoing-integrations', ['admin']).then(() => { - request + void updatePermission('manage-outgoing-integrations', []).then(() => { + void updatePermission('manage-own-outgoing-integrations', ['admin']).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -209,8 +211,8 @@ describe('[Outgoing Integrations]', function () { }); it('should create an outgoing integration successfully', (done) => { - let integrationId; - request + let integrationId: IIntegration['_id']; + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -244,14 +246,17 @@ describe('[Outgoing Integrations]', function () { describe('[/integrations.list]', () => { it('should return the list of outgoing integrations', (done) => { - request + void request .get(api('integrations.list')) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreatedByAdmin = res.body.integrations.find((createdIntegration) => createdIntegration._id === integration._id); + const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( + (createdIntegration) => createdIntegration._id === integration._id, + ); + assert.isDefined(integrationCreatedByAdmin); expect(integrationCreatedByAdmin).to.be.an('object'); expect(integrationCreatedByAdmin._id).to.be.equal(integration._id); expect(res.body).to.have.property('offset'); @@ -262,16 +267,16 @@ describe('[Outgoing Integrations]', function () { }); it('should return the list create by the user only', (done) => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-own-outgoing-integrations', ['user']).then(() => { - request + void updatePermission('manage-outgoing-integrations', []).then(() => { + void updatePermission('manage-own-outgoing-integrations', ['user']).then(() => { + void request .get(api('integrations.list')) .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreatedByAdmin = res.body.integrations.find( + const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integration._id, ); expect(integrationCreatedByAdmin).to.be.equal(undefined); @@ -285,11 +290,11 @@ describe('[Outgoing Integrations]', function () { }); it('should return unauthorized error when the user does not have any integrations permissions', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-outgoing-integrations', []).then(() => { - request + void updatePermission('manage-incoming-integrations', []).then(() => { + void updatePermission('manage-own-incoming-integrations', []).then(() => { + void updatePermission('manage-outgoing-integrations', []).then(() => { + void updatePermission('manage-outgoing-integrations', []).then(() => { + void request .get(api('integrations.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -308,9 +313,9 @@ describe('[Outgoing Integrations]', function () { describe('[/integrations.history]', () => { it('should return an error when the user DOES NOT the necessary permission', (done) => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-own-outgoing-integrations', []).then(() => { - request + void updatePermission('manage-outgoing-integrations', []).then(() => { + void updatePermission('manage-own-outgoing-integrations', []).then(() => { + void request .get(api('integrations.history')) .set(credentials) .query({ @@ -328,8 +333,8 @@ describe('[Outgoing Integrations]', function () { }); it('should return the history of outgoing integrations', (done) => { - updatePermission('manage-outgoing-integrations', ['admin']).then(() => { - request + void updatePermission('manage-outgoing-integrations', ['admin']).then(() => { + void request .get(api('integrations.history')) .set(credentials) .query({ @@ -351,7 +356,7 @@ describe('[Outgoing Integrations]', function () { describe('[/integrations.get]', () => { it('should return an error when the required "integrationId" query parameters is not sent', (done) => { - request + void request .get(api('integrations.get')) .set(credentials) .expect('Content-Type', 'application/json') @@ -364,9 +369,10 @@ describe('[Outgoing Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to get an outgoing integration', (done) => { - updatePermission('manage-outgoing-integrations', []).then(() => { - request - .get(api(`integrations.get?integrationId=${integration._id}`)) + void updatePermission('manage-outgoing-integrations', []).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -379,9 +385,10 @@ describe('[Outgoing Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to get an outgoing integration created by another user', (done) => { - updatePermission('manage-outgoing-integrations', []).then(() => { - request - .get(api(`integrations.get?integrationId=${integrationCreatedByAnUser._id}`)) + void updatePermission('manage-outgoing-integrations', []).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: integrationCreatedByAnUser._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -394,9 +401,10 @@ describe('[Outgoing Integrations]', function () { }); it('should return an error when the user sends an invalid integration', (done) => { - updatePermission('manage-outgoing-integrations', ['admin']).then(() => { - request - .get(api('integrations.get?integrationId=invalid')) + void updatePermission('manage-outgoing-integrations', ['admin']).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -409,11 +417,12 @@ describe('[Outgoing Integrations]', function () { }); it('should return the integration successfully when the user is able to see only your own integrations', (done) => { - updatePermission('manage-outgoing-integrations', []) + void updatePermission('manage-outgoing-integrations', []) .then(() => updatePermission('manage-own-outgoing-integrations', ['user'])) .then(() => { - request - .get(api(`integrations.get?integrationId=${integrationCreatedByAnUser._id}`)) + void request + .get(api('integrations.get')) + .query({ integrationId: integrationCreatedByAnUser._id }) .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200) @@ -427,9 +436,10 @@ describe('[Outgoing Integrations]', function () { }); it('should return the integration successfully', (done) => { - updatePermission('manage-outgoing-integrations', ['admin']).then(() => { - request - .get(api(`integrations.get?integrationId=${integration._id}`)) + void updatePermission('manage-outgoing-integrations', ['admin']).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -445,8 +455,8 @@ describe('[Outgoing Integrations]', function () { describe('[/integrations.remove]', () => { it('should return an error when the user DOES NOT have the permission "manage-outgoing-integrations" to remove an outgoing integration', (done) => { - updatePermission('manage-outgoing-integrations', []).then(() => { - request + void updatePermission('manage-outgoing-integrations', []).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -464,8 +474,8 @@ describe('[Outgoing Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-own-outgoing-integrations" to remove an outgoing integration', (done) => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - request + void updatePermission('manage-own-incoming-integrations', []).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -483,8 +493,8 @@ describe('[Outgoing Integrations]', function () { }); it('should return an error when the user sends an invalid type of integration', (done) => { - updatePermission('manage-own-outgoing-integrations', ['admin']).then(() => { - request + void updatePermission('manage-own-outgoing-integrations', ['admin']).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -502,8 +512,8 @@ describe('[Outgoing Integrations]', function () { }); it('should remove the integration successfully when the user at least one of the necessary permission to remove an outgoing integration', (done) => { - updatePermission('manage-outgoing-integrations', ['admin']).then(() => { - request + void updatePermission('manage-outgoing-integrations', ['admin']).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -520,8 +530,8 @@ describe('[Outgoing Integrations]', function () { }); it('the normal user should remove the integration successfully when the user have the "manage-own-outgoing-integrations" to remove an outgoing integration', (done) => { - updatePermission('manage-own-outgoing-integrations', ['user']).then(() => { - request + void updatePermission('manage-own-outgoing-integrations', ['user']).then(() => { + void request .post(api('integrations.remove')) .set(userCredentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/07-incoming-integrations.js b/apps/meteor/tests/end-to-end/api/07-incoming-integrations.ts similarity index 81% rename from apps/meteor/tests/end-to-end/api/07-incoming-integrations.js rename to apps/meteor/tests/end-to-end/api/07-incoming-integrations.ts index 7c325a9e3cd25..ff475945a9ac6 100644 --- a/apps/meteor/tests/end-to-end/api/07-incoming-integrations.js +++ b/apps/meteor/tests/end-to-end/api/07-incoming-integrations.ts @@ -1,22 +1,23 @@ -import { expect } from 'chai'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IIntegration, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { assert, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('[Incoming Integrations]', function () { - this.retries(0); - - let integration; - let integrationCreatedByAnUser; - let user; - let userCredentials; - let channel; - let testChannelName; +describe('[Incoming Integrations]', () => { + let integration: IIntegration; + let integrationCreatedByAnUser: IIntegration; + let user: TestUser; + let userCredentials: Credentials; + let channel: IRoom; + let testChannelName: string; before((done) => getCredentials(done)); @@ -46,8 +47,8 @@ describe('[Incoming Integrations]', function () { describe('[/integrations.create]', () => { it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to add an incoming integration', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - request + void updatePermission('manage-incoming-integrations', []).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -71,8 +72,8 @@ describe('[Incoming Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to add an incoming integration', (done) => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - request + void updatePermission('manage-own-incoming-integrations', []).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -96,7 +97,7 @@ describe('[Incoming Integrations]', function () { }); it('should return an error when the user sends an invalid type of integration', (done) => { - request + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -119,9 +120,9 @@ describe('[Incoming Integrations]', function () { }); it('should add the integration successfully when the user ONLY has the permission "manage-incoming-integrations" to add an incoming integration', (done) => { - let integrationId; - updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - request + let integrationId: IIntegration['_id']; + void updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -146,8 +147,7 @@ describe('[Incoming Integrations]', function () { }); it('should set overrideDestinationChannelEnabled setting to false when it is not provided', async () => { - let integrationId; - await request + const res = await request .post(api('integrations.create')) .set(credentials) .send({ @@ -160,20 +160,19 @@ describe('[Incoming Integrations]', function () { channel: '#general', }) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('integration').and.to.be.an('object'); - expect(res.body.integration).to.have.property('overrideDestinationChannelEnabled', false); - integrationId = res.body.integration._id; - }); + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + expect(res.body.integration).to.have.property('overrideDestinationChannelEnabled', false); + const integrationId = res.body.integration._id; await removeIntegration(integrationId, 'incoming'); }); it('should add the integration successfully when the user ONLY has the permission "manage-own-incoming-integrations" to add an incoming integration', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - request + void updatePermission('manage-incoming-integrations', []).then(() => { + void updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { + void request .post(api('integrations.create')) .set(credentials) .send({ @@ -199,7 +198,7 @@ describe('[Incoming Integrations]', function () { }); it('should execute the incoming integration', (done) => { - request + void request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: 'Example message', @@ -209,7 +208,7 @@ describe('[Incoming Integrations]', function () { }); it("should return an error when sending 'channel' field telling its configuration is disabled", (done) => { - request + void request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: 'Example message', @@ -224,7 +223,7 @@ describe('[Incoming Integrations]', function () { }); it("should return an error when sending 'roomId' field telling its configuration is disabled", (done) => { - request + void request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: 'Example message', @@ -239,7 +238,7 @@ describe('[Incoming Integrations]', function () { }); it('should send a message for a channel that is specified in the webhooks configuration', (done) => { const successfulMesssage = `Message sent successfully at #${Date.now()}`; - request + void request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: successfulMesssage, @@ -257,7 +256,7 @@ describe('[Incoming Integrations]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!res.body.messages.find((m) => m.msg === successfulMesssage)).to.be.true; + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.true; }) .end(done); }); @@ -303,14 +302,14 @@ describe('[Incoming Integrations]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!res.body.messages.find((m) => m.msg === successfulMesssage)).to.be.true; + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.true; }); }); }); describe('[/integrations.history]', () => { it('should return an error when trying to get history of incoming integrations', (done) => { - request + void request .get(api('integrations.history')) .set(credentials) .query({ @@ -328,12 +327,12 @@ describe('[Incoming Integrations]', function () { describe('[/integrations.list]', () => { before((done) => { - createUser().then((createdUser) => { + void createUser().then((createdUser) => { user = createdUser; - login(user.username, password).then((credentials) => { + void login(user.username, password).then((credentials) => { userCredentials = credentials; - updatePermission('manage-incoming-integrations', ['user']).then(() => { - createIntegration( + void updatePermission('manage-incoming-integrations', ['user']).then(() => { + void createIntegration( { type: 'webhook-incoming', name: 'Incoming test', @@ -355,14 +354,17 @@ describe('[Incoming Integrations]', function () { }); it('should return the list of incoming integrations', (done) => { - request + void request .get(api('integrations.list')) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreatedByAdmin = res.body.integrations.find((createdIntegration) => createdIntegration._id === integration._id); + const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( + (createdIntegration) => createdIntegration._id === integration._id, + ); + assert.isDefined(integrationCreatedByAdmin); expect(integrationCreatedByAdmin).to.be.an('object'); expect(integrationCreatedByAdmin._id).to.be.equal(integration._id); expect(res.body).to.have.property('offset'); @@ -373,16 +375,16 @@ describe('[Incoming Integrations]', function () { }); it('should return the list of integrations created by the user only', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - updatePermission('manage-own-incoming-integrations', ['user']).then(() => { - request + void updatePermission('manage-incoming-integrations', []).then(() => { + void updatePermission('manage-own-incoming-integrations', ['user']).then(() => { + void request .get(api('integrations.list')) .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); - const integrationCreatedByAdmin = res.body.integrations.find( + const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integration._id, ); expect(integrationCreatedByAdmin).to.be.equal(undefined); @@ -396,11 +398,11 @@ describe('[Incoming Integrations]', function () { }); it('should return unauthorized error when the user does not have any integrations permissions', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - updatePermission('manage-outgoing-integrations', []).then(() => { - updatePermission('manage-outgoing-integrations', []).then(() => { - request + void updatePermission('manage-incoming-integrations', []).then(() => { + void updatePermission('manage-own-incoming-integrations', []).then(() => { + void updatePermission('manage-outgoing-integrations', []).then(() => { + void updatePermission('manage-outgoing-integrations', []).then(() => { + void request .get(api('integrations.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -419,7 +421,7 @@ describe('[Incoming Integrations]', function () { describe('[/integrations.get]', () => { it('should return an error when the required "integrationId" query parameters is not sent', (done) => { - request + void request .get(api('integrations.get')) .set(credentials) .expect('Content-Type', 'application/json') @@ -432,9 +434,10 @@ describe('[Incoming Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - request - .get(api(`integrations.get?integrationId=${integration._id}`)) + void updatePermission('manage-incoming-integrations', []).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -447,9 +450,10 @@ describe('[Incoming Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration created by another user', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - request - .get(api(`integrations.get?integrationId=${integrationCreatedByAnUser._id}`)) + void updatePermission('manage-incoming-integrations', []).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: integrationCreatedByAnUser._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -462,9 +466,10 @@ describe('[Incoming Integrations]', function () { }); it('should return an error when the user sends an invalid integration', (done) => { - updatePermission('manage-incoming-integrations', ['admin']).then(() => { - request - .get(api('integrations.get?integrationId=invalid')) + void updatePermission('manage-incoming-integrations', ['admin']).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -477,11 +482,12 @@ describe('[Incoming Integrations]', function () { }); it('should return the integration successfully when the user is able to see only your own integrations', (done) => { - updatePermission('manage-incoming-integrations', []) + void updatePermission('manage-incoming-integrations', []) .then(() => updatePermission('manage-own-incoming-integrations', ['user'])) .then(() => { - request - .get(api(`integrations.get?integrationId=${integrationCreatedByAnUser._id}`)) + void request + .get(api('integrations.get')) + .query({ integrationId: integrationCreatedByAnUser._id }) .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200) @@ -495,9 +501,10 @@ describe('[Incoming Integrations]', function () { }); it('should return the integration successfully', (done) => { - updatePermission('manage-incoming-integrations', ['admin']).then(() => { - request - .get(api(`integrations.get?integrationId=${integration._id}`)) + void updatePermission('manage-incoming-integrations', ['admin']).then(() => { + void request + .get(api('integrations.get')) + .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -513,7 +520,7 @@ describe('[Incoming Integrations]', function () { describe('[/integrations.update]', () => { it('should update an integration by id and return the new data', (done) => { - request + void request .put(api('integrations.update')) .set(credentials) .send({ @@ -540,8 +547,9 @@ describe('[Incoming Integrations]', function () { }); it('should have integration updated on subsequent gets', (done) => { - request - .get(api(`integrations.get?integrationId=${integration._id}`)) + void request + .get(api('integrations.get')) + .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -558,8 +566,8 @@ describe('[Incoming Integrations]', function () { describe('[/integrations.remove]', () => { it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to remove an incoming integration', (done) => { - updatePermission('manage-incoming-integrations', []).then(() => { - request + void updatePermission('manage-incoming-integrations', []).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -577,8 +585,8 @@ describe('[Incoming Integrations]', function () { }); it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to remove an incoming integration', (done) => { - updatePermission('manage-own-incoming-integrations', []).then(() => { - request + void updatePermission('manage-own-incoming-integrations', []).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -596,8 +604,8 @@ describe('[Incoming Integrations]', function () { }); it('should return an error when the user sends an invalid type of integration', (done) => { - updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - request + void updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -615,8 +623,8 @@ describe('[Incoming Integrations]', function () { }); it('should remove the integration successfully when the user at least one of the necessary permission to remove an incoming integration', (done) => { - updatePermission('manage-incoming-integrations', ['admin']).then(() => { - request + void updatePermission('manage-incoming-integrations', ['admin']).then(() => { + void request .post(api('integrations.remove')) .set(credentials) .send({ @@ -633,8 +641,8 @@ describe('[Incoming Integrations]', function () { }); it('the normal user should remove the integration successfully when the user have the "manage-own-incoming-integrations" to remove an incoming integration', (done) => { - updatePermission('manage-own-incoming-integrations', ['user']).then(() => { - request + void updatePermission('manage-own-incoming-integrations', ['user']).then(() => { + void request .post(api('integrations.remove')) .set(userCredentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/08-settings.js b/apps/meteor/tests/end-to-end/api/08-settings.ts similarity index 89% rename from apps/meteor/tests/end-to-end/api/08-settings.js rename to apps/meteor/tests/end-to-end/api/08-settings.ts index 4ea6a6ae778e1..3a03ecbe92263 100644 --- a/apps/meteor/tests/end-to-end/api/08-settings.js +++ b/apps/meteor/tests/end-to-end/api/08-settings.ts @@ -1,17 +1,16 @@ +import type { LoginServiceConfiguration } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updateSetting } from '../../data/permissions.helper'; -describe('[Settings]', function () { - this.retries(0); - +describe('[Settings]', () => { before((done) => getCredentials(done)); describe('[/settings.public]', () => { it('should return public settings', (done) => { - request + void request .get(api('settings.public')) .expect('Content-Type', 'application/json') .expect(200) @@ -23,7 +22,7 @@ describe('[Settings]', function () { .end(done); }); it('should return public settings even requested with count and offset params', (done) => { - request + void request .get(api('settings.public')) .query({ count: 5, @@ -42,7 +41,7 @@ describe('[Settings]', function () { describe('[/settings]', () => { it('should return private settings', (done) => { - request + void request .get(api('settings')) .set(credentials) .expect('Content-Type', 'application/json') @@ -58,7 +57,7 @@ describe('[Settings]', function () { describe('[/settings/:_id]', () => { it('should return one setting', (done) => { - request + void request .get(api('settings/Site_Url')) .set(credentials) .expect('Content-Type', 'application/json') @@ -74,7 +73,7 @@ describe('[Settings]', function () { describe('[/service.configurations]', () => { it('should return service configurations', (done) => { - request + void request .get(api('service.configurations')) .set(credentials) .expect('Content-Type', 'application/json') @@ -94,7 +93,7 @@ describe('[Settings]', function () { it('should include the OAuth service in the response', (done) => { // wait 3 seconds before getting the service list so the server has had time to update it setTimeout(() => { - request + void request .get(api('service.configurations')) .set(credentials) .expect('Content-Type', 'application/json') @@ -103,7 +102,7 @@ describe('[Settings]', function () { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('configurations'); - expect(res.body.configurations.find(({ service }) => service === 'google')).to.exist; + expect((res.body.configurations as LoginServiceConfiguration[]).find(({ service }) => service === 'google')).to.exist; }) .end(done); }, 3000); @@ -118,7 +117,7 @@ describe('[Settings]', function () { it('should not include the OAuth service in the response', (done) => { // wait 3 seconds before getting the service list so the server has had time to update it setTimeout(() => { - request + void request .get(api('service.configurations')) .set(credentials) .expect('Content-Type', 'application/json') @@ -127,7 +126,7 @@ describe('[Settings]', function () { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('configurations'); - expect(res.body.configurations.find(({ service }) => service === 'google')).to.not.exist; + expect((res.body.configurations as LoginServiceConfiguration[]).find(({ service }) => service === 'google')).to.not.exist; }) .end(done); }, 3000); @@ -137,7 +136,7 @@ describe('[Settings]', function () { describe('/settings.oauth', () => { it('should have return list of available oauth services when user is not logged', (done) => { - request + void request .get(api('settings.oauth')) .expect('Content-Type', 'application/json') .expect(200) @@ -149,7 +148,7 @@ describe('[Settings]', function () { }); it('should have return list of available oauth services when user is logged', (done) => { - request + void request .get(api('settings.oauth')) .set(credentials) .expect('Content-Type', 'application/json') diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.ts similarity index 91% rename from apps/meteor/tests/end-to-end/api/09-rooms.js rename to apps/meteor/tests/end-to-end/api/09-rooms.ts index de4668e86f485..03d33ecd3b8d1 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.ts @@ -1,27 +1,33 @@ import fs from 'fs'; import path from 'path'; -import { expect } from 'chai'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IMessage, IRoom, ITeam, IUpload, IUser, ImageAttachmentProps, SettingValue } from '@rocket.chat/core-typings'; +import { assert, expect } from 'chai'; import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; import { sleep } from '../../../lib/utils/sleep'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; -import { drawioURL, imgURL, lstURL, svgLogoFileName, svgLogoURL } from '../../data/interactions'; +import { imgURL } from '../../data/interactions'; import { getSettingValueById, updateEEPermission, updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { deleteTeam } from '../../data/teams.helper'; import { password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; import { IS_EE } from '../../e2e/config/constants'; -describe('[Rooms]', function () { - this.retries(0); +const lstURL = './tests/e2e/fixtures/files/lst-test.lst'; +const drawioURL = './tests/e2e/fixtures/files/diagram.drawio'; +const svgLogoURL = './public/images/logo/logo.svg'; +const svgLogoFileName = 'logo.svg'; +describe('[Rooms]', () => { before((done) => getCredentials(done)); it('/rooms.get', (done) => { - request + void request .get(api('rooms.get')) .set(credentials) .expect(200) @@ -34,7 +40,7 @@ describe('[Rooms]', function () { }); it('/rooms.get?updatedSince', (done) => { - request + void request .get(api('rooms.get')) .set(credentials) .query({ @@ -50,7 +56,7 @@ describe('[Rooms]', function () { }); describe('/rooms.saveNotification:', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}-${Math.random()}` })).body.channel; @@ -59,7 +65,7 @@ describe('[Rooms]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('/rooms.saveNotification:', (done) => { - request + void request .post(api('rooms.saveNotification')) .set(credentials) .send({ @@ -82,12 +88,12 @@ describe('[Rooms]', function () { }); describe('/rooms.upload', () => { - let testChannel; - let user; - let userCredentials; + let testChannel: IRoom; + let user: TestUser; + let userCredentials: Credentials; const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`; - let blockedMediaTypes; - let testPrivateChannel; + let blockedMediaTypes: SettingValue; + let testPrivateChannel: IRoom; before(async () => { user = await createUser({ joinDefaultChannels: false }); @@ -95,7 +101,7 @@ describe('[Rooms]', function () { testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; testPrivateChannel = (await createRoom({ type: 'p', name: `channel.test.private.${Date.now()}-${Math.random()}` })).body.group; blockedMediaTypes = await getSettingValueById('FileUpload_MediaTypeBlackList'); - const newBlockedMediaTypes = blockedMediaTypes + const newBlockedMediaTypes = (blockedMediaTypes as string) .split(',') .filter((type) => type !== 'image/svg+xml') .join(','); @@ -115,7 +121,7 @@ describe('[Rooms]', function () { ); it("don't upload a file to room with file field other than file", (done) => { - request + void request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .attach('test', imgURL) @@ -129,7 +135,7 @@ describe('[Rooms]', function () { .end(done); }); it("don't upload a file to room with empty file", (done) => { - request + void request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .attach('file', '') @@ -142,7 +148,7 @@ describe('[Rooms]', function () { .end(done); }); it("don't upload a file to room with more than 1 file", (done) => { - request + void request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .attach('file', imgURL) @@ -156,8 +162,8 @@ describe('[Rooms]', function () { .end(done); }); - let fileNewUrl; - let fileOldUrl; + let fileNewUrl: string; + let fileOldUrl: string; it('should upload a PNG file to room', async () => { await request .post(api(`rooms.upload/${testChannel._id}`)) @@ -166,7 +172,7 @@ describe('[Rooms]', function () { .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { - const { message } = res.body; + const message = res.body.message as IMessage; expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('message'); expect(res.body.message).to.have.property('attachments'); @@ -178,6 +184,7 @@ describe('[Rooms]', function () { expect(res.body.message.files[0]).to.have.property('type', 'image/png'); expect(res.body.message.files[0]).to.have.property('name', '1024x1024.png'); + assert.isDefined(message.file); fileNewUrl = `/file-upload/${message.file._id}/${message.file.name}`; fileOldUrl = `/ufs/GridFS:Uploads/${message.file._id}/${message.file.name}`; }); @@ -319,52 +326,52 @@ describe('[Rooms]', function () { it('should generate thumbnail for SVG files correctly', async () => { const expectedFileName = `thumb-${svgLogoFileName}`; - let thumbUrl; - await request + + const res = await request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .attach('file', svgLogoURL) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - const { message } = res.body; - const { files, attachments } = message; + .expect(200); - expect(files).to.be.an('array'); - const hasThumbFile = files.some((file) => file.type === 'image/png' && file.name === expectedFileName); - expect(hasThumbFile).to.be.true; + const message = res.body.message as IMessage; + const { files, attachments } = message; - expect(attachments).to.be.an('array'); - const thumbAttachment = attachments.find((attachment) => attachment.title === svgLogoFileName); - expect(thumbAttachment).to.be.an('object'); - thumbUrl = thumbAttachment.image_url; - }); + expect(files).to.be.an('array'); + const hasThumbFile = files?.some((file) => file.type === 'image/png' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; + + expect(attachments).to.be.an('array'); + const thumbAttachment = attachments?.find((attachment) => attachment.title === svgLogoFileName); + assert.isDefined(thumbAttachment); + expect(thumbAttachment).to.be.an('object'); + const thumbUrl = (thumbAttachment as ImageAttachmentProps).image_url; await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/png'); }); it('should generate thumbnail for JPEG files correctly', async () => { const expectedFileName = `thumb-sample-jpeg.jpg`; - let thumbUrl; - await request + const res = await request .post(api(`rooms.upload/${testChannel._id}`)) .set(credentials) .attach('file', fs.createReadStream(path.join(__dirname, '../../mocks/files/sample-jpeg.jpg'))) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - const { message } = res.body; - const { files, attachments } = message; + .expect(200); - expect(files).to.be.an('array'); - const hasThumbFile = files.some((file) => file.type === 'image/jpeg' && file.name === expectedFileName); - expect(hasThumbFile).to.be.true; + const message = res.body.message as IMessage; + const { files, attachments } = message; - expect(attachments).to.be.an('array'); - const thumbAttachment = attachments.find((attachment) => attachment.title === `sample-jpeg.jpg`); - expect(thumbAttachment).to.be.an('object'); - thumbUrl = thumbAttachment.image_url; - }); + expect(files).to.be.an('array'); + assert.isDefined(files); + const hasThumbFile = files.some((file) => file.type === 'image/jpeg' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; + + expect(attachments).to.be.an('array'); + assert.isDefined(attachments); + const thumbAttachment = attachments.find((attachment) => attachment.title === `sample-jpeg.jpg`); + expect(thumbAttachment).to.be.an('object'); + const thumbUrl = (thumbAttachment as ImageAttachmentProps).image_url; await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/jpeg'); }); @@ -395,18 +402,18 @@ describe('[Rooms]', function () { }); describe('/rooms.media', () => { - let testChannel; - let user; - let userCredentials; + let testChannel: IRoom; + let user: TestUser; + let userCredentials: Credentials; const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`; - let blockedMediaTypes; + let blockedMediaTypes: SettingValue; before(async () => { user = await createUser({ joinDefaultChannels: false }); userCredentials = await login(user.username, password); testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; blockedMediaTypes = await getSettingValueById('FileUpload_MediaTypeBlackList'); - const newBlockedMediaTypes = blockedMediaTypes + const newBlockedMediaTypes = (blockedMediaTypes as string) .split(',') .filter((type) => type !== 'image/svg+xml') .join(','); @@ -424,7 +431,7 @@ describe('[Rooms]', function () { ); it("don't upload a file to room with file field other than file", (done) => { - request + void request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) .attach('test', imgURL) @@ -438,7 +445,7 @@ describe('[Rooms]', function () { .end(done); }); it("don't upload a file to room with empty file", (done) => { - request + void request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) .attach('file', '') @@ -451,7 +458,7 @@ describe('[Rooms]', function () { .end(done); }); it("don't upload a file to room with more than 1 file", (done) => { - request + void request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) .attach('file', imgURL) @@ -465,9 +472,9 @@ describe('[Rooms]', function () { .end(done); }); - let fileNewUrl; - let fileOldUrl; - let fileId; + let fileNewUrl: string; + let fileOldUrl: string; + let fileId: string; it('should upload a PNG file to room', async () => { await request .post(api(`rooms.media/${testChannel._id}`)) @@ -592,82 +599,71 @@ describe('[Rooms]', function () { it('should generate thumbnail for SVG files correctly', async () => { const expectedFileName = `thumb-${svgLogoFileName}`; - let thumbUrl; - let fileId; - await request + let res = await request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) .attach('file', svgLogoURL) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('file'); - expect(res.body.file).to.have.property('_id'); - expect(res.body.file).to.have.property('url'); + .expect(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); - fileId = res.body.file._id; - }); + const fileId = res.body.file._id; - await request + res = await request .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) .set(credentials) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - const { message } = res.body; - const { files, attachments } = message; + .expect(200); + const message = res.body.message as IMessage; + const { files, attachments } = message; - expect(files).to.be.an('array'); - const hasThumbFile = files.some((file) => file.type === 'image/png' && file.name === expectedFileName); - expect(hasThumbFile).to.be.true; + expect(files).to.be.an('array'); + const hasThumbFile = files?.some((file) => file.type === 'image/png' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; - expect(attachments).to.be.an('array'); - const thumbAttachment = attachments.find((attachment) => attachment.title === svgLogoFileName); - expect(thumbAttachment).to.be.an('object'); - thumbUrl = thumbAttachment.image_url; - }); + expect(attachments).to.be.an('array'); + const thumbAttachment = attachments?.find((attachment) => attachment.title === svgLogoFileName); + assert.isDefined(thumbAttachment); + expect(thumbAttachment).to.be.an('object'); + const thumbUrl = (thumbAttachment as ImageAttachmentProps).image_url; await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/png'); }); it('should generate thumbnail for JPEG files correctly', async () => { const expectedFileName = `thumb-sample-jpeg.jpg`; - let thumbUrl; - let fileId; - await request + let res = await request .post(api(`rooms.media/${testChannel._id}`)) .set(credentials) .attach('file', fs.createReadStream(path.join(__dirname, '../../mocks/files/sample-jpeg.jpg'))) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('file'); - expect(res.body.file).to.have.property('_id'); - expect(res.body.file).to.have.property('url'); + .expect(200); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('file'); + expect(res.body.file).to.have.property('_id'); + expect(res.body.file).to.have.property('url'); - fileId = res.body.file._id; - }); + const fileId = res.body.file._id; - await request + res = await request .post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`)) .set(credentials) .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - const { message } = res.body; - const { files, attachments } = message; + .expect(200); + const message = res.body.message as IMessage; + const { files, attachments } = message; - expect(files).to.be.an('array'); - const hasThumbFile = files.some((file) => file.type === 'image/jpeg' && file.name === expectedFileName); - expect(hasThumbFile).to.be.true; + expect(files).to.be.an('array'); + const hasThumbFile = files?.some((file) => file.type === 'image/jpeg' && file.name === expectedFileName); + expect(hasThumbFile).to.be.true; - expect(attachments).to.be.an('array'); - const thumbAttachment = attachments.find((attachment) => attachment.title === `sample-jpeg.jpg`); - expect(thumbAttachment).to.be.an('object'); - thumbUrl = thumbAttachment.image_url; - }); + expect(attachments).to.be.an('array'); + const thumbAttachment = attachments?.find((attachment) => attachment.title === `sample-jpeg.jpg`); + expect(thumbAttachment).to.be.an('object'); + const thumbUrl = (thumbAttachment as ImageAttachmentProps).image_url; await request.get(thumbUrl).set(credentials).expect('Content-Type', 'image/jpeg'); }); @@ -715,7 +711,7 @@ describe('[Rooms]', function () { }); describe('/rooms.favorite', () => { - let testChannel; + let testChannel: IRoom; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; before(async () => { @@ -725,7 +721,7 @@ describe('[Rooms]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should favorite the room when send favorite: true by roomName', (done) => { - request + void request .post(api('rooms.favorite')) .set(credentials) .send({ @@ -739,7 +735,7 @@ describe('[Rooms]', function () { .end(done); }); it('should unfavorite the room when send favorite: false by roomName', (done) => { - request + void request .post(api('rooms.favorite')) .set(credentials) .send({ @@ -753,7 +749,7 @@ describe('[Rooms]', function () { .end(done); }); it('should favorite the room when send favorite: true by roomId', (done) => { - request + void request .post(api('rooms.favorite')) .set(credentials) .send({ @@ -768,7 +764,7 @@ describe('[Rooms]', function () { }); it('should unfavorite room when send favorite: false by roomId', (done) => { - request + void request .post(api('rooms.favorite')) .set(credentials) .send({ @@ -783,7 +779,7 @@ describe('[Rooms]', function () { }); it('should return an error when send an invalid room', (done) => { - request + void request .post(api('rooms.favorite')) .set(credentials) .send({ @@ -800,7 +796,7 @@ describe('[Rooms]', function () { }); describe('/rooms.nameExists', () => { - let testChannel; + let testChannel: IRoom; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; before(async () => { @@ -810,7 +806,7 @@ describe('[Rooms]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should return 401 unauthorized when user is not logged in', (done) => { - request + void request .get(api('rooms.nameExists')) .expect('Content-Type', 'application/json') .expect(401) @@ -821,7 +817,7 @@ describe('[Rooms]', function () { }); it('should return true if this room name exists', (done) => { - request + void request .get(api('rooms.nameExists')) .set(credentials) .query({ @@ -836,7 +832,7 @@ describe('[Rooms]', function () { }); it('should return an error when send an invalid room', (done) => { - request + void request .get(api('rooms.nameExists')) .set(credentials) .query({ @@ -852,11 +848,11 @@ describe('[Rooms]', function () { }); describe('[/rooms.cleanHistory]', () => { - let publicChannel; - let privateChannel; - let directMessageChannelId; - let user; - let userCredentials; + let publicChannel: IRoom; + let privateChannel: IRoom; + let directMessageChannelId: IRoom['_id']; + let user: TestUser; + let userCredentials: Credentials; beforeEach(async () => { user = await createUser(); @@ -880,7 +876,7 @@ describe('[Rooms]', function () { after(() => updateSetting('Message_ShowDeletedStatus', false)); it('should return success when send a valid public channel', (done) => { - request + void request .post(api('rooms.cleanHistory')) .set(credentials) .send({ @@ -933,22 +929,23 @@ describe('[Rooms]', function () { }); }); it('should successfully delete an image and thumbnail from public channel', (done) => { - request + void request .post(api(`rooms.upload/${publicChannel._id}`)) .set(credentials) .attach('file', imgURL) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { - const { message } = res.body; + const message = res.body.message as IMessage; expect(res.body).to.have.property('success', true); expect(res.body).to.have.nested.property('message._id', message._id); expect(res.body).to.have.nested.property('message.rid', publicChannel._id); + assert.isDefined(message.file); expect(res.body).to.have.nested.property('message.file._id', message.file._id); expect(res.body).to.have.nested.property('message.file.type', message.file.type); }); - request + void request .post(api('rooms.cleanHistory')) .set(credentials) .send({ @@ -962,7 +959,7 @@ describe('[Rooms]', function () { expect(res.body).to.have.property('success', true); }); - request + void request .get(api('channels.files')) .set(credentials) .query({ @@ -978,7 +975,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return success when send a valid private channel', (done) => { - request + void request .post(api('rooms.cleanHistory')) .set(credentials) .send({ @@ -994,7 +991,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return success when send a valid Direct Message channel', (done) => { - request + void request .post(api('rooms.cleanHistory')) .set(credentials) .send({ @@ -1010,7 +1007,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return not allowed error when try deleting messages with user without permission', (done) => { - request + void request .post(api('rooms.cleanHistory')) .set(userCredentials) .send({ @@ -1029,9 +1026,9 @@ describe('[Rooms]', function () { }); describe('[/rooms.info]', () => { - let testChannel; - let testGroup; - let testDM; + let testChannel: IRoom; + let testGroup: IRoom; + let testDM: IRoom; const expectedKeys = [ '_id', 'name', @@ -1065,7 +1062,7 @@ describe('[Rooms]', function () { ); it('should return the info about the created channel correctly searching by roomId', (done) => { - request + void request .get(api('rooms.info')) .set(credentials) .query({ @@ -1080,7 +1077,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return the info about the created channel correctly searching by roomName', (done) => { - request + void request .get(api('rooms.info')) .set(credentials) .query({ @@ -1095,7 +1092,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return the info about the created group correctly searching by roomId', (done) => { - request + void request .get(api('rooms.info')) .set(credentials) .query({ @@ -1110,7 +1107,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return the info about the created group correctly searching by roomName', (done) => { - request + void request .get(api('rooms.info')) .set(credentials) .query({ @@ -1125,7 +1122,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return the info about the created DM correctly searching by roomId', (done) => { - request + void request .get(api('rooms.info')) .set(credentials) .query({ @@ -1139,7 +1136,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return name and _id of public channel when it has the "fields" query parameter limiting by name', (done) => { - request + void request .get(api('rooms.info')) .set(credentials) .query({ @@ -1158,11 +1155,11 @@ describe('[Rooms]', function () { }); describe('[/rooms.leave]', () => { - let testChannel; - let testGroup; - let testDM; - let user2; - let user2Credentials; + let testChannel: IRoom; + let testGroup: IRoom; + let testDM: IRoom; + let user2: TestUser; + let user2Credentials: Credentials; const testChannelName = `channel.leave.${Date.now()}-${Math.random()}`; const testGroupName = `group.leave.${Date.now()}-${Math.random()}`; @@ -1186,7 +1183,7 @@ describe('[Rooms]', function () { ); it('should return an Error when trying leave a DM room', (done) => { - request + void request .post(api('rooms.leave')) .set(credentials) .send({ @@ -1200,7 +1197,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return an Error when trying to leave a public channel and you are the last owner', (done) => { - request + void request .post(api('rooms.leave')) .set(credentials) .send({ @@ -1214,7 +1211,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return an Error when trying to leave a private group and you are the last owner', (done) => { - request + void request .post(api('rooms.leave')) .set(credentials) .send({ @@ -1228,8 +1225,8 @@ describe('[Rooms]', function () { .end(done); }); it('should return an Error when trying to leave a public channel and not have the necessary permission(leave-c)', (done) => { - updatePermission('leave-c', []).then(() => { - request + void updatePermission('leave-c', []).then(() => { + void request .post(api('rooms.leave')) .set(credentials) .send({ @@ -1244,8 +1241,8 @@ describe('[Rooms]', function () { }); }); it('should return an Error when trying to leave a private group and not have the necessary permission(leave-p)', (done) => { - updatePermission('leave-p', []).then(() => { - request + void updatePermission('leave-p', []).then(() => { + void request .post(api('rooms.leave')) .set(credentials) .send({ @@ -1309,10 +1306,10 @@ describe('[Rooms]', function () { }); describe('/rooms.createDiscussion', () => { - let testChannel; + let testChannel: IRoom; const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; - let messageSent; - let privateTeam; + let messageSent: IMessage; + let privateTeam: ITeam; before(async () => { testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; @@ -1334,8 +1331,8 @@ describe('[Rooms]', function () { ); it('should throw an error when the user tries to create a discussion and the feature is disabled', (done) => { - updateSetting('Discussion_enabled', false).then(() => { - request + void updateSetting('Discussion_enabled', false).then(() => { + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1351,9 +1348,9 @@ describe('[Rooms]', function () { }); }); it('should throw an error when the user tries to create a discussion and does not have at least one of the required permissions', (done) => { - updatePermission('start-discussion', []).then(() => { - updatePermission('start-discussion-other-user', []).then(() => { - request + void updatePermission('start-discussion', []).then(() => { + void updatePermission('start-discussion-other-user', []).then(() => { + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1366,7 +1363,7 @@ describe('[Rooms]', function () { expect(res.body).to.have.property('errorType', 'error-action-not-allowed'); }) .end(() => { - updatePermission('start-discussion', ['admin', 'user', 'guest']) + void updatePermission('start-discussion', ['admin', 'user', 'guest']) .then(() => updatePermission('start-discussion-other-user', ['admin', 'user', 'guest'])) .then(done); }); @@ -1374,7 +1371,7 @@ describe('[Rooms]', function () { }); }); it('should throw an error when the user tries to create a discussion without the required parameter "prid"', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({}) @@ -1386,7 +1383,7 @@ describe('[Rooms]', function () { .end(done); }); it('should throw an error when the user tries to create a discussion without the required parameter "t_name"', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1400,7 +1397,7 @@ describe('[Rooms]', function () { .end(done); }); it('should throw an error when the user tries to create a discussion with the required parameter invalid "users"(different from an array)', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1416,7 +1413,7 @@ describe('[Rooms]', function () { .end(done); }); it("should throw an error when the user tries to create a discussion with the channel's id invalid", (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1431,7 +1428,7 @@ describe('[Rooms]', function () { .end(done); }); it("should throw an error when the user tries to create a discussion with the message's id invalid", (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1447,7 +1444,7 @@ describe('[Rooms]', function () { .end(done); }); it('should create a discussion successfully when send only the required parameters', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1464,7 +1461,7 @@ describe('[Rooms]', function () { .end(done); }); it('should create a discussion successfully when send the required parameters plus the optional parameter "reply"', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1482,7 +1479,7 @@ describe('[Rooms]', function () { .end(done); }); it('should create a discussion successfully when send the required parameters plus the optional parameter "users"', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1501,7 +1498,7 @@ describe('[Rooms]', function () { .end(done); }); it('should create a discussion successfully when send the required parameters plus the optional parameter "pmid"', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1523,7 +1520,7 @@ describe('[Rooms]', function () { describe('it should create a *private* discussion if the parent channel is public and inside a private team', async () => { it('should create a team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -1542,7 +1539,7 @@ describe('[Rooms]', function () { }); it('should add the public channel to the team', (done) => { - request + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -1558,7 +1555,7 @@ describe('[Rooms]', function () { }); it('should create a private discussion inside the public channel', (done) => { - request + void request .post(api('rooms.createDiscussion')) .set(credentials) .send({ @@ -1579,7 +1576,7 @@ describe('[Rooms]', function () { }); describe('/rooms.getDiscussions', () => { - let testChannel; + let testChannel: IRoom; const testChannelName = `channel.test.getDiscussions${Date.now()}-${Math.random()}`; before(async () => { @@ -1601,7 +1598,7 @@ describe('[Rooms]', function () { ); it('should throw an error when the user tries to gets a list of discussion without a required parameter "roomId"', (done) => { - request + void request .get(api('rooms.getDiscussions')) .set(credentials) .query({}) @@ -1613,8 +1610,8 @@ describe('[Rooms]', function () { .end(done); }); it('should throw an error when the user tries to gets a list of discussion and he cannot access the room', (done) => { - updatePermission('view-c-room', []).then(() => { - request + void updatePermission('view-c-room', []).then(() => { + void request .get(api('rooms.getDiscussions')) .set(credentials) .query({}) @@ -1627,7 +1624,7 @@ describe('[Rooms]', function () { }); }); it('should return a list of discussions with ONE discussion', (done) => { - request + void request .get(api('rooms.getDiscussions')) .set(credentials) .query({ @@ -1645,7 +1642,7 @@ describe('[Rooms]', function () { describe('[/rooms.autocomplete.channelAndPrivate]', () => { it('should return an error when the required parameter "selector" is not provided', (done) => { - request + void request .get(api('rooms.autocomplete.channelAndPrivate')) .set(credentials) .query({}) @@ -1658,8 +1655,9 @@ describe('[Rooms]', function () { .end(done); }); it('should return the rooms to fill auto complete', (done) => { - request - .get(api('rooms.autocomplete.channelAndPrivate?selector={}')) + void request + .get(api('rooms.autocomplete.channelAndPrivate')) + .query({ selector: '{}' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -1673,7 +1671,7 @@ describe('[Rooms]', function () { describe('[/rooms.autocomplete.channelAndPrivate.withPagination]', () => { it('should return an error when the required parameter "selector" is not provided', (done) => { - request + void request .get(api('rooms.autocomplete.channelAndPrivate.withPagination')) .set(credentials) .query({}) @@ -1686,8 +1684,9 @@ describe('[Rooms]', function () { .end(done); }); it('should return the rooms to fill auto complete', (done) => { - request - .get(api('rooms.autocomplete.channelAndPrivate.withPagination?selector={}')) + void request + .get(api('rooms.autocomplete.channelAndPrivate.withPagination')) + .query({ selector: '{}' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -1699,8 +1698,9 @@ describe('[Rooms]', function () { .end(done); }); it('should return the rooms to fill auto complete even requested with count and offset params', (done) => { - request - .get(api('rooms.autocomplete.channelAndPrivate.withPagination?selector={}')) + void request + .get(api('rooms.autocomplete.channelAndPrivate.withPagination')) + .query({ selector: '{}' }) .set(credentials) .query({ count: 5, @@ -1719,7 +1719,7 @@ describe('[Rooms]', function () { describe('[/rooms.autocomplete.availableForTeams]', () => { it('should return the rooms to fill auto complete', (done) => { - request + void request .get(api('rooms.autocomplete.availableForTeams')) .set(credentials) .expect('Content-Type', 'application/json') @@ -1731,8 +1731,9 @@ describe('[Rooms]', function () { .end(done); }); it('should return the filtered rooms to fill auto complete', (done) => { - request - .get(api('rooms.autocomplete.availableForTeams?name=group')) + void request + .get(api('rooms.autocomplete.availableForTeams')) + .query({ name: 'group' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -1745,7 +1746,7 @@ describe('[Rooms]', function () { }); describe('[/rooms.autocomplete.adminRooms]', () => { - let testGroup; + let testGroup: IRoom; const testGroupName = `channel.test.adminRoom${Date.now()}-${Math.random()}`; const name = { name: testGroupName, @@ -1765,8 +1766,8 @@ describe('[Rooms]', function () { after(() => Promise.all([deleteRoom({ type: 'p', roomId: testGroup._id }), updateEEPermission('can-audit', ['admin', 'auditor'])])); (IS_EE ? it : it.skip)('should return an error when the required parameter "selector" is not provided', (done) => { - updateEEPermission('can-audit', ['admin']).then(() => { - request + void updateEEPermission('can-audit', ['admin']).then(() => { + void request .get(api('rooms.autocomplete.adminRooms')) .set(credentials) .query({}) @@ -1780,8 +1781,9 @@ describe('[Rooms]', function () { }); }); it('should return the rooms to fill auto complete', (done) => { - request - .get(api('rooms.autocomplete.adminRooms?selector={}')) + void request + .get(api('rooms.autocomplete.adminRooms')) + .query({ selector: '{}' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -1792,8 +1794,8 @@ describe('[Rooms]', function () { .end(done); }); it('should return the rooms to fill auto complete', (done) => { - request - .get(api('rooms.autocomplete.adminRooms?')) + void request + .get(api('rooms.autocomplete.adminRooms')) .set(credentials) .query({ selector: JSON.stringify(name), @@ -1815,7 +1817,7 @@ describe('[Rooms]', function () { const nameRoom = `Ellinika-${suffix}`; const discussionRoomName = `${nameRoom}-discussion`; - let testGroup; + let testGroup: IRoom; before(async () => { await updateSetting('UI_Allow_room_names_with_special_chars', true); @@ -1835,8 +1837,8 @@ describe('[Rooms]', function () { ); it('should throw an error when the user tries to gets a list of discussion and he cannot access the room', (done) => { - updatePermission('view-room-administration', []).then(() => { - request + void updatePermission('view-room-administration', []).then(() => { + void request .get(api('rooms.adminRooms')) .set(credentials) .expect(400) @@ -1848,7 +1850,7 @@ describe('[Rooms]', function () { }); }); it('should return a list of admin rooms', (done) => { - request + void request .get(api('rooms.adminRooms')) .set(credentials) .expect(200) @@ -1862,7 +1864,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return a list of admin rooms even requested with count and offset params', (done) => { - request + void request .get(api('rooms.adminRooms')) .set(credentials) .query({ @@ -1880,8 +1882,8 @@ describe('[Rooms]', function () { .end(done); }); it('should search the list of admin rooms using non-latin characters when UI_Allow_room_names_with_special_chars setting is toggled', (done) => { - updateSetting('UI_Allow_room_names_with_special_chars', true).then(() => { - request + void updateSetting('UI_Allow_room_names_with_special_chars', true).then(() => { + void request .get(api('rooms.adminRooms')) .set(credentials) .query({ @@ -1901,8 +1903,8 @@ describe('[Rooms]', function () { }); }); it('should search the list of admin rooms using latin characters only when UI_Allow_room_names_with_special_chars setting is disabled', (done) => { - updateSetting('UI_Allow_room_names_with_special_chars', false).then(() => { - request + void updateSetting('UI_Allow_room_names_with_special_chars', false).then(() => { + void request .get(api('rooms.adminRooms')) .set(credentials) .query({ @@ -1922,7 +1924,7 @@ describe('[Rooms]', function () { }); }); it('should filter by only rooms types', (done) => { - request + void request .get(api('rooms.adminRooms')) .set(credentials) .query({ @@ -1934,13 +1936,13 @@ describe('[Rooms]', function () { expect(res.body).to.have.property('rooms').and.to.be.an('array'); expect(res.body.rooms).to.have.lengthOf.at.least(1); expect(res.body.rooms[0].t).to.be.equal('p'); - expect(res.body.rooms.find((room) => room.name === nameRoom)).to.exist; - expect(res.body.rooms.find((room) => room.name === discussionRoomName)).to.not.exist; + expect((res.body.rooms as IRoom[]).find((room) => room.name === nameRoom)).to.exist; + expect((res.body.rooms as IRoom[]).find((room) => room.name === discussionRoomName)).to.not.exist; }) .end(done); }); it('should filter by only name', (done) => { - request + void request .get(api('rooms.adminRooms')) .set(credentials) .query({ @@ -1956,7 +1958,7 @@ describe('[Rooms]', function () { .end(done); }); it('should filter by type and name at the same query', (done) => { - request + void request .get(api('rooms.adminRooms')) .set(credentials) .query({ @@ -1973,7 +1975,7 @@ describe('[Rooms]', function () { .end(done); }); it('should return an empty array when filter by wrong type and correct room name', (done) => { - request + void request .get(api('rooms.adminRooms')) .set(credentials) .query({ @@ -1991,8 +1993,8 @@ describe('[Rooms]', function () { }); describe('update group dms name', () => { - let testUser; - let roomId; + let testUser: TestUser; + let roomId: IRoom['_id']; before(async () => { testUser = await createUser(); @@ -2063,7 +2065,7 @@ describe('[Rooms]', function () { }); describe('/rooms.delete', () => { - let testChannel; + let testChannel: IRoom; before('create an channel', async () => { const result = await createRoom({ type: 'c', name: `channel.test.${Date.now()}-${Math.random()}` }); @@ -2073,7 +2075,7 @@ describe('[Rooms]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should throw an error when roomId is not provided', (done) => { - request + void request .post(api('rooms.delete')) .set(credentials) .send({}) @@ -2086,7 +2088,7 @@ describe('[Rooms]', function () { .end(done); }); it('should delete a room when the request is correct', (done) => { - request + void request .post(api('rooms.delete')) .set(credentials) .send({ roomId: testChannel._id }) @@ -2098,7 +2100,7 @@ describe('[Rooms]', function () { .end(done); }); it('should throw an error when the room id doesn exist', (done) => { - request + void request .post(api('rooms.delete')) .set(credentials) .send({ roomId: 'invalid' }) @@ -2112,9 +2114,9 @@ describe('[Rooms]', function () { }); describe('rooms.saveRoomSettings', () => { - let testChannel; + let testChannel: IRoom; const randomString = `randomString${Date.now()}`; - let discussion; + let discussion: IRoom; before(async () => { const result = await createRoom({ type: 'c', name: `channel.test.${Date.now()}-${Math.random()}` }); @@ -2136,7 +2138,7 @@ describe('[Rooms]', function () { it('should update the room settings', (done) => { const imageDataUri = `data:image/png;base64,${fs.readFileSync(path.join(process.cwd(), imgURL)).toString('base64')}`; - request + void request .post(api('rooms.saveRoomSettings')) .set(credentials) .send({ @@ -2162,7 +2164,7 @@ describe('[Rooms]', function () { }); it('should have reflected on rooms.info', (done) => { - request + void request .get(api('rooms.info')) .set(credentials) .query({ @@ -2278,13 +2280,19 @@ describe('[Rooms]', function () { }); describe('rooms.images', () => { - let testUserCreds = null; + let testUserCreds: Credentials; before(async () => { const user = await createUser(); testUserCreds = await login(user.username, password); }); - const uploadFile = async ({ roomId, file }) => { + const uploadFile = async ({ + roomId, + file, + }: { + roomId: IRoom['_id']; + file: Blob | Buffer | fs.ReadStream | string | boolean | number; + }) => { const { body } = await request .post(api(`rooms.upload/${roomId}`)) .set(credentials) @@ -2295,7 +2303,7 @@ describe('[Rooms]', function () { return body.message.attachments[0]; }; - const getIdFromImgPath = (link) => { + const getIdFromImgPath = (link: string) => { return link.split('/')[2]; }; @@ -2319,7 +2327,7 @@ describe('[Rooms]', function () { await deleteRoom({ type: 'p', roomId }); }); it('should return an empty array when room is valid and user is part of it but there are no images', async () => { - const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` }); + const { body } = await createRoom({ type: 'p', name: `test-${Date.now()}` }); const { group: { _id: roomId }, } = body; @@ -2336,7 +2344,7 @@ describe('[Rooms]', function () { await deleteRoom({ type: 'p', roomId }); }); it('should return an array of images when room is valid and user is part of it and there are images', async () => { - const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` }); + const { body } = await createRoom({ type: 'p', name: `test-${Date.now()}` }); const { group: { _id: roomId }, } = body; @@ -2359,7 +2367,7 @@ describe('[Rooms]', function () { await deleteRoom({ type: 'p', roomId }); }); it('should return multiple images when room is valid and user is part of it and there are multiple images', async () => { - const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` }); + const { body } = await createRoom({ type: 'p', name: `test-${Date.now()}` }); const { group: { _id: roomId }, } = body; @@ -2383,14 +2391,14 @@ describe('[Rooms]', function () { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('files').and.to.be.an('array').and.to.have.lengthOf(2); - expect(res.body.files.find((file) => file._id === fileId1)).to.exist; - expect(res.body.files.find((file) => file._id === fileId2)).to.exist; + expect((res.body.files as IUpload[]).find((file) => file._id === fileId1)).to.exist; + expect((res.body.files as IUpload[]).find((file) => file._id === fileId2)).to.exist; }); await deleteRoom({ type: 'p', roomId }); }); it('should allow to filter images passing the startingFromId parameter', async () => { - const { body } = await createRoom({ type: 'p', usernames: [credentials.username], name: `test-${Date.now()}` }); + const { body } = await createRoom({ type: 'p', name: `test-${Date.now()}` }); const { group: { _id: roomId }, } = body; @@ -2420,7 +2428,7 @@ describe('[Rooms]', function () { }); describe('/rooms.muteUser', () => { - let testChannel; + let testChannel: IRoom; before('create a channel', async () => { const result = await createRoom({ type: 'c', name: `channel.test.${Date.now()}-${Math.random()}` }); @@ -2482,7 +2490,7 @@ describe('[Rooms]', function () { }); describe('/rooms.unmuteUser', () => { - let testChannel; + let testChannel: IRoom; before('create a channel', async () => { const result = await createRoom({ type: 'c', name: `channel.test.${Date.now()}-${Math.random()}` }); @@ -2555,8 +2563,8 @@ describe('[Rooms]', function () { }); describe('/rooms.export', () => { - let testChannel; - let testMessageId; + let testChannel: IRoom; + let testMessageId: IMessage['_id']; before(async () => { const result = await createRoom({ type: 'c', name: `channel.export.test.${Date.now()}-${Math.random()}` }); diff --git a/apps/meteor/tests/end-to-end/api/10-subscriptions.js b/apps/meteor/tests/end-to-end/api/10-subscriptions.ts similarity index 92% rename from apps/meteor/tests/end-to-end/api/10-subscriptions.js rename to apps/meteor/tests/end-to-end/api/10-subscriptions.ts index e541026216908..da73196022715 100644 --- a/apps/meteor/tests/end-to-end/api/10-subscriptions.js +++ b/apps/meteor/tests/end-to-end/api/10-subscriptions.ts @@ -1,17 +1,18 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { adminUsername } from '../../data/user'; -import { createUser, deleteUser, login } from '../../data/users.helper.js'; - -describe('[Subscriptions]', function () { - this.retries(0); +import type { TestUser } from '../../data/users.helper'; +import { createUser, deleteUser, login } from '../../data/users.helper'; +describe('[Subscriptions]', () => { before((done) => getCredentials(done)); - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel; @@ -20,7 +21,7 @@ describe('[Subscriptions]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('/subscriptions.get', (done) => { - request + void request .get(api('subscriptions.get')) .set(credentials) .expect('Content-Type', 'application/json') @@ -34,7 +35,7 @@ describe('[Subscriptions]', function () { }); it('/subscriptions.get?updatedSince', (done) => { - request + void request .get(api('subscriptions.get')) .set(credentials) .query({ @@ -51,7 +52,7 @@ describe('[Subscriptions]', function () { it('/subscriptions.getOne:', () => { it('subscriptions.getOne', (done) => { - request + void request .get(api('subscriptions.getOne')) .set(credentials) .query({ @@ -68,9 +69,9 @@ describe('[Subscriptions]', function () { }); describe('[/subscriptions.read]', () => { - let testChannel; - let testGroup; - let testDM; + let testChannel: IRoom; + let testGroup: IRoom; + let testDM: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel; @@ -87,7 +88,7 @@ describe('[Subscriptions]', function () { ); it('should mark public channels as read', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -101,7 +102,7 @@ describe('[Subscriptions]', function () { }); it('should mark groups as read', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -115,7 +116,7 @@ describe('[Subscriptions]', function () { }); it('should mark DMs as read', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -129,7 +130,7 @@ describe('[Subscriptions]', function () { }); it('should fail on two params with different ids', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -145,7 +146,7 @@ describe('[Subscriptions]', function () { }); it('should fail on mark inexistent public channel as read', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -160,7 +161,7 @@ describe('[Subscriptions]', function () { }); it('should fail on mark inexistent group as read', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -175,7 +176,7 @@ describe('[Subscriptions]', function () { }); it('should fail on mark inexistent DM as read', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -190,7 +191,7 @@ describe('[Subscriptions]', function () { }); it('should fail on invalid params', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({ @@ -205,7 +206,7 @@ describe('[Subscriptions]', function () { }); it('should fail on empty params', (done) => { - request + void request .post(api('subscriptions.read')) .set(credentials) .send({}) @@ -218,9 +219,9 @@ describe('[Subscriptions]', function () { }); describe('should handle threads correctly', () => { - let threadId; - let user; - let threadUserCredentials; + let threadId: IThreadMessage['_id']; + let user: TestUser; + let threadUserCredentials: Credentials; before(async () => { user = await createUser({ username: 'testthread123', password: 'testthread123' }); @@ -320,7 +321,7 @@ describe('[Subscriptions]', function () { }); describe('[/subscriptions.unread]', () => { - let testChannel; + let testChannel: IRoom; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.${Date.now()}` })).body.channel; @@ -329,7 +330,7 @@ describe('[Subscriptions]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should fail when there are no messages on an channel', (done) => { - request + void request .post(api('subscriptions.unread')) .set(credentials) .send({ @@ -344,7 +345,7 @@ describe('[Subscriptions]', function () { .end(done); }); it('sending message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -362,7 +363,7 @@ describe('[Subscriptions]', function () { .end(done); }); it('should return success: true when make as unread successfully', (done) => { - request + void request .post(api('subscriptions.unread')) .set(credentials) .send({ @@ -376,7 +377,7 @@ describe('[Subscriptions]', function () { }); it('should fail on invalid params', (done) => { - request + void request .post(api('subscriptions.unread')) .set(credentials) .send({ @@ -391,7 +392,7 @@ describe('[Subscriptions]', function () { }); it('should fail on empty params', (done) => { - request + void request .post(api('subscriptions.unread')) .set(credentials) .send({}) diff --git a/apps/meteor/tests/end-to-end/api/11-permissions.js b/apps/meteor/tests/end-to-end/api/11-permissions.ts similarity index 91% rename from apps/meteor/tests/end-to-end/api/11-permissions.js rename to apps/meteor/tests/end-to-end/api/11-permissions.ts index 30db3b5b67e27..55bd724dad45b 100644 --- a/apps/meteor/tests/end-to-end/api/11-permissions.js +++ b/apps/meteor/tests/end-to-end/api/11-permissions.ts @@ -1,19 +1,17 @@ import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updatePermission } from '../../data/permissions.helper'; -describe('[Permissions]', function () { - this.retries(0); - +describe('[Permissions]', () => { before((done) => getCredentials(done)); after(() => updatePermission('add-oauth-service', ['admin'])); describe('[/permissions.listAll]', () => { it('should return an array with update and remove properties', (done) => { - request + void request .get(api('permissions.listAll')) .set(credentials) .expect('Content-Type', 'application/json') @@ -27,8 +25,9 @@ describe('[Permissions]', function () { }); it('should return an array with update and remov properties when search by "updatedSince" query parameter', (done) => { - request - .get(api('permissions.listAll?updatedSince=2018-11-27T13:52:01Z')) + void request + .get(api('permissions.listAll')) + .query({ updatedSince: '2018-11-27T13:52:01Z' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -41,8 +40,9 @@ describe('[Permissions]', function () { }); it('should return an error when updatedSince query parameter is not a valid ISODate string', (done) => { - request - .get(api('permissions.listAll?updatedSince=fsafdf')) + void request + .get(api('permissions.listAll')) + .query({ updatedSince: 'fsafdf' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -61,7 +61,7 @@ describe('[Permissions]', function () { roles: ['admin', 'user'], }, ]; - request + void request .post(api('permissions.update')) .set(credentials) .send({ permissions }) @@ -85,7 +85,7 @@ describe('[Permissions]', function () { roles: ['admin'], }, ]; - request + void request .post(api('permissions.update')) .set(credentials) .send({ permissions }) @@ -103,7 +103,7 @@ describe('[Permissions]', function () { roles: ['this-role-does-not-exist'], }, ]; - request + void request .post(api('permissions.update')) .set(credentials) .send({ permissions }) @@ -116,7 +116,7 @@ describe('[Permissions]', function () { }); it('should 400 when trying to set permissions to a string', (done) => { const permissions = ''; - request + void request .post(api('permissions.update')) .set(credentials) .send({ permissions }) diff --git a/apps/meteor/tests/end-to-end/api/12-emoji-custom.js b/apps/meteor/tests/end-to-end/api/12-emoji-custom.ts similarity index 87% rename from apps/meteor/tests/end-to-end/api/12-emoji-custom.js rename to apps/meteor/tests/end-to-end/api/12-emoji-custom.ts index 488be47852ca1..31042363836e3 100644 --- a/apps/meteor/tests/end-to-end/api/12-emoji-custom.js +++ b/apps/meteor/tests/end-to-end/api/12-emoji-custom.ts @@ -1,16 +1,14 @@ -import { expect } from 'chai'; +import type { ICustomEmojiDescriptor } from '@rocket.chat/core-typings'; +import { assert, expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { imgURL } from '../../data/interactions'; -const customEmojiName = `my-custom-emoji-${Date.now()}`; -let createdCustomEmoji; +describe('[EmojiCustom]', () => { + const customEmojiName = `my-custom-emoji-${Date.now()}`; -describe('[EmojiCustom]', function () { - let withoutAliases; - - this.retries(0); + let withoutAliases: ICustomEmojiDescriptor; before((done) => getCredentials(done)); @@ -22,7 +20,7 @@ describe('[EmojiCustom]', function () { describe('[/emoji-custom.create]', () => { it('should create new custom emoji', (done) => { - request + void request .post(api('emoji-custom.create')) .set(credentials) .attach('emoji', imgURL) @@ -38,7 +36,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should create new custom emoji without optional parameter "aliases"', (done) => { - request + void request .post(api('emoji-custom.create')) .set(credentials) .attach('emoji', imgURL) @@ -53,7 +51,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should throw an error when the filename is wrong', (done) => { - request + void request .post(api('emoji-custom.create')) .set(credentials) .attach('emojiwrong', imgURL) @@ -71,9 +69,11 @@ describe('[EmojiCustom]', function () { }); }); + let createdCustomEmoji: ICustomEmojiDescriptor; + describe('[/emoji-custom.update]', () => { before((done) => { - request + void request .get(api('emoji-custom.list')) .set(credentials) .expect(200) @@ -82,14 +82,22 @@ describe('[EmojiCustom]', function () { expect(res.body.emojis).to.have.property('update').and.to.be.a('array').and.to.not.have.lengthOf(0); expect(res.body.emojis).to.have.property('remove').and.to.be.a('array').and.to.have.lengthOf(0); - createdCustomEmoji = res.body.emojis.update.find((emoji) => emoji.name === customEmojiName); - withoutAliases = res.body.emojis.update.find((emoji) => emoji.name === `${customEmojiName}-without-aliases`); + const _createdCustomEmoji = (res.body.emojis.update as ICustomEmojiDescriptor[]).find((emoji) => emoji.name === customEmojiName); + const _withoutAliases = (res.body.emojis.update as ICustomEmojiDescriptor[]).find( + (emoji) => emoji.name === `${customEmojiName}-without-aliases`, + ); + + assert.isDefined(_createdCustomEmoji); + assert.isDefined(_withoutAliases); + + createdCustomEmoji = _createdCustomEmoji; + withoutAliases = _withoutAliases; }) .end(done); }); it('successfully:', () => { it('should update the custom emoji without a file', (done) => { - request + void request .post(api('emoji-custom.update')) .set(credentials) .field({ @@ -105,7 +113,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should update the custom emoji without optional parameter "aliases"', (done) => { - request + void request .post(api('emoji-custom.update')) .set(credentials) .field({ @@ -120,7 +128,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should update the custom emoji with all parameters and with a file', (done) => { - request + void request .post(api('emoji-custom.update')) .set(credentials) .attach('emoji', imgURL) @@ -138,7 +146,7 @@ describe('[EmojiCustom]', function () { }); it('should throw error when:', () => { it('the fields does not include "_id"', (done) => { - request + void request .post(api('emoji-custom.update')) .set(credentials) .attach('emoji', imgURL) @@ -154,7 +162,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('the custom emoji does not exists', (done) => { - request + void request .post(api('emoji-custom.update')) .set(credentials) .attach('emoji', imgURL) @@ -171,7 +179,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('the filename is wrong', (done) => { - request + void request .post(api('emoji-custom.update')) .set(credentials) .attach('emojiwrong', imgURL) @@ -192,7 +200,7 @@ describe('[EmojiCustom]', function () { describe('[/emoji-custom.list]', () => { it('should return emojis', (done) => { - request + void request .get(api('emoji-custom.list')) .set(credentials) .expect(200) @@ -204,8 +212,9 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should return emojis when use "query" query parameter', (done) => { - request - .get(api(`emoji-custom.list?query={"_updatedAt": {"$gt": { "$date": "${new Date().toISOString()}" } } }`)) + void request + .get(api('emoji-custom.list')) + .query({ query: `{ "_updatedAt": { "$gt": { "$date": "${new Date().toISOString()}" } } }` }) .set(credentials) .expect(200) .expect((res) => { @@ -217,8 +226,9 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should return emojis when use "updateSince" query parameter', (done) => { - request - .get(api(`emoji-custom.list?updatedSince=${new Date().toISOString()}`)) + void request + .get(api('emoji-custom.list')) + .query({ updatedSince: new Date().toISOString() }) .set(credentials) .expect(200) .expect((res) => { @@ -230,12 +240,9 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should return emojis when use both, "updateSince" and "query" query parameter', (done) => { - request - .get( - api( - `emoji-custom.list?query={"_updatedAt": {"$gt": { "$date": "${new Date().toISOString()}" } }}&updatedSince=${new Date().toISOString()}`, - ), - ) + void request + .get(api('emoji-custom.list')) + .query({ query: `{"_updatedAt": {"$gt": { "$date": "${new Date().toISOString()}" } }}`, updatedSince: new Date().toISOString() }) .set(credentials) .expect(200) .expect((res) => { @@ -247,8 +254,9 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should return an error when the "updateSince" query parameter is a invalid date', (done) => { - request - .get(api('emoji-custom.list?updatedSince=invalid-date')) + void request + .get(api('emoji-custom.list')) + .query({ updatedSince: 'invalid-date' }) .set(credentials) .expect(400) .expect((res) => { @@ -261,7 +269,7 @@ describe('[EmojiCustom]', function () { describe('[/emoji-custom.all]', () => { it('should return emojis', (done) => { - request + void request .get(api('emoji-custom.all')) .set(credentials) .expect(200) @@ -274,7 +282,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should return emojis even requested with count and offset params', (done) => { - request + void request .get(api('emoji-custom.all')) .set(credentials) .query({ @@ -293,10 +301,10 @@ describe('[EmojiCustom]', function () { }); describe('Accessing custom emojis', () => { - let uploadDate; + let uploadDate: unknown; it('should return forbidden if the there is no fileId on the url', (done) => { - request + void request .get('/emoji-custom/') .set(credentials) .expect(403) @@ -307,7 +315,7 @@ describe('[EmojiCustom]', function () { }); it('should return success if the file does not exists with some specific headers', (done) => { - request + void request .get('/emoji-custom/invalid') .set(credentials) .expect(200) @@ -322,7 +330,7 @@ describe('[EmojiCustom]', function () { }); it('should return not modified if the file does not exists and if-modified-since is equal to the Thu, 01 Jan 2015 00:00:00 GMT', (done) => { - request + void request .get('/emoji-custom/invalid') .set(credentials) .set({ @@ -336,7 +344,7 @@ describe('[EmojiCustom]', function () { }); it('should return success if the the requested exists', (done) => { - request + void request .get(`/emoji-custom/${customEmojiName}.png`) .set(credentials) .expect(200) @@ -351,7 +359,7 @@ describe('[EmojiCustom]', function () { }); it('should return not modified if the the requested file contains a valid-since equal to the upload date', (done) => { - request + void request .get(`/emoji-custom/${customEmojiName}.png`) .set(credentials) .set({ @@ -370,7 +378,7 @@ describe('[EmojiCustom]', function () { describe('[/emoji-custom.delete]', () => { it('should throw an error when trying delete custom emoji without the required param "emojid"', (done) => { - request + void request .post(api('emoji-custom.delete')) .set(credentials) .send({}) @@ -383,7 +391,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should throw an error when trying delete custom emoji that does not exists', (done) => { - request + void request .post(api('emoji-custom.delete')) .set(credentials) .send({ @@ -398,7 +406,7 @@ describe('[EmojiCustom]', function () { .end(done); }); it('should delete the custom emoji created before successfully', (done) => { - request + void request .post(api('emoji-custom.delete')) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/14-assets.js b/apps/meteor/tests/end-to-end/api/14-assets.ts similarity index 94% rename from apps/meteor/tests/end-to-end/api/14-assets.js rename to apps/meteor/tests/end-to-end/api/14-assets.ts index 3bcc968e7ee77..ff9dbfa7a89fa 100644 --- a/apps/meteor/tests/end-to-end/api/14-assets.js +++ b/apps/meteor/tests/end-to-end/api/14-assets.ts @@ -1,13 +1,11 @@ import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { imgURL } from '../../data/interactions'; import { updatePermission } from '../../data/permissions.helper'; -describe('[Assets]', function () { - this.retries(0); - +describe('[Assets]', () => { before((done) => getCredentials(done)); before(() => updatePermission('manage-assets', ['admin'])); @@ -16,7 +14,7 @@ describe('[Assets]', function () { describe('[/assets.setAsset]', () => { it('should set the "logo" asset', (done) => { - request + void request .post(api('assets.setAsset')) .set(credentials) .attach('asset', imgURL) @@ -31,7 +29,7 @@ describe('[Assets]', function () { .end(done); }); it('should throw an error when we try set an invalid asset', (done) => { - request + void request .post(api('assets.setAsset')) .set(credentials) .attach('invalidAsset', imgURL) @@ -46,7 +44,7 @@ describe('[Assets]', function () { describe('[/assets.unsetAsset]', () => { it('should unset the "logo" asset', (done) => { - request + void request .post(api('assets.unsetAsset')) .set(credentials) .send({ @@ -60,7 +58,7 @@ describe('[Assets]', function () { .end(done); }); it('should throw an error when we try set an invalid asset', (done) => { - request + void request .post(api('assets.unsetAsset')) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/16-commands.js b/apps/meteor/tests/end-to-end/api/16-commands.ts similarity index 94% rename from apps/meteor/tests/end-to-end/api/16-commands.js rename to apps/meteor/tests/end-to-end/api/16-commands.ts index c0781167b67fe..36a0842cc58fe 100644 --- a/apps/meteor/tests/end-to-end/api/16-commands.js +++ b/apps/meteor/tests/end-to-end/api/16-commands.ts @@ -1,20 +1,21 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; -import { sendSimpleMessage } from '../../data/chat.helper.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { sendSimpleMessage } from '../../data/chat.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; -import { createUser, deleteUser, login } from '../../data/users.helper.js'; - -describe('[Commands]', function () { - this.retries(0); +import type { TestUser } from '../../data/users.helper'; +import { createUser, deleteUser, login } from '../../data/users.helper'; +describe('[Commands]', () => { before((done) => getCredentials(done)); describe('[/commands.get]', () => { it('should return an error when call the endpoint without "command" required parameter', (done) => { - request + void request .get(api('commands.get')) .set(credentials) .expect(400) @@ -25,7 +26,7 @@ describe('[Commands]', function () { .end(done); }); it('should return an error when call the endpoint with an invalid command', (done) => { - request + void request .get(api('commands.get')) .set(credentials) .query({ @@ -39,7 +40,7 @@ describe('[Commands]', function () { .end(done); }); it('should return success when parameters are correct', (done) => { - request + void request .get(api('commands.get')) .set(credentials) .query({ @@ -94,8 +95,8 @@ describe('[Commands]', function () { }); describe('[/commands.run]', () => { - let testChannel; - let threadMessage; + let testChannel: IRoom; + let threadMessage: IThreadMessage; before(async () => { testChannel = (await createRoom({ type: 'c', name: `channel.test.commands.${Date.now()}` })).body.channel; @@ -116,7 +117,7 @@ describe('[Commands]', function () { after(() => deleteRoom({ type: 'c', roomId: testChannel._id })); it('should return an error when call the endpoint without "command" required parameter', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .expect(400) @@ -127,7 +128,7 @@ describe('[Commands]', function () { .end(done); }); it('should return an error when call the endpoint with the param "params" and it is not a string', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .send({ @@ -142,7 +143,7 @@ describe('[Commands]', function () { .end(done); }); it('should return an error when call the endpoint without "roomId" required parameter', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .send({ @@ -157,7 +158,7 @@ describe('[Commands]', function () { .end(done); }); it('should return an error when call the endpoint with the param "tmid" and it is not a string', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .send({ @@ -174,7 +175,7 @@ describe('[Commands]', function () { .end(done); }); it('should return an error when call the endpoint with the invalid "command" param', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .send({ @@ -190,7 +191,7 @@ describe('[Commands]', function () { .end(done); }); it('should return an error when call the endpoint with an invalid thread id', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .send({ @@ -207,7 +208,7 @@ describe('[Commands]', function () { .end(done); }); it('should return an error when call the endpoint with a valid thread id of wrong channel', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .send({ @@ -224,7 +225,7 @@ describe('[Commands]', function () { .end(done); }); it('should return success when parameters are correct', (done) => { - request + void request .post(api('commands.run')) .set(credentials) .send({ @@ -243,8 +244,8 @@ describe('[Commands]', function () { describe('Command archive', function () { describe('unauthorized cases', () => { - let user; - let credentials; + let user: TestUser; + let credentials: Credentials; this.beforeAll(async () => { user = await createUser({ @@ -319,8 +320,8 @@ describe('[Commands]', function () { describe('Command unarchive', function () { describe('unauthorized cases', () => { - let user; - let credentials; + let user: TestUser; + let credentials: Credentials; this.beforeAll(async () => { user = await createUser({ joinDefaultChannels: true, diff --git a/apps/meteor/tests/end-to-end/api/17-custom-sounds.js b/apps/meteor/tests/end-to-end/api/17-custom-sounds.ts similarity index 94% rename from apps/meteor/tests/end-to-end/api/17-custom-sounds.js rename to apps/meteor/tests/end-to-end/api/17-custom-sounds.ts index 006129a31d7c8..11ba7622bfe09 100644 --- a/apps/meteor/tests/end-to-end/api/17-custom-sounds.js +++ b/apps/meteor/tests/end-to-end/api/17-custom-sounds.ts @@ -5,16 +5,14 @@ import path from 'path'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; - -describe('[CustomSounds]', function () { - this.retries(0); +import { getCredentials, api, request, credentials } from '../../data/api-data'; +describe('[CustomSounds]', () => { before((done) => getCredentials(done)); describe('[/custom-sounds.list]', () => { it('should return custom sounds', (done) => { - request + void request .get(api('custom-sounds.list')) .set(credentials) .expect(200) @@ -27,7 +25,7 @@ describe('[CustomSounds]', function () { .end(done); }); it('should return custom sounds even requested with count and offset params', (done) => { - request + void request .get(api('custom-sounds.list')) .set(credentials) .expect(200) @@ -46,13 +44,13 @@ describe('[CustomSounds]', function () { }); describe('Accessing custom sounds', () => { - let fileId; + let fileId: string; const fileName = `test-file-${randomUUID()}`; - let uploadDate; + let uploadDate: unknown; before(async () => { const data = readFileSync(path.resolve(__dirname, '../../mocks/files/audio_mock.wav')); - const binary = Buffer.from(data, 'base64').toString('binary'); + const binary = data.toString('binary'); await request .post(api('method.call/insertOrUpdateSound')) .set(credentials) @@ -97,7 +95,7 @@ describe('[CustomSounds]', function () { ); it('should return forbidden if the there is no fileId on the url', (done) => { - request + void request .get('/custom-sounds/') .set(credentials) .expect(403) @@ -108,7 +106,7 @@ describe('[CustomSounds]', function () { }); it('should return not found if the the requested file does not exists', (done) => { - request + void request .get('/custom-sounds/invalid.mp3') .set(credentials) .expect(404) @@ -119,7 +117,7 @@ describe('[CustomSounds]', function () { }); it('should return success if the the requested exists', (done) => { - request + void request .get(`/custom-sounds/${fileId}.wav`) .set(credentials) .expect(200) @@ -134,7 +132,7 @@ describe('[CustomSounds]', function () { }); it('should return not modified if the the requested file contains a valid-since equal to the upload date', (done) => { - request + void request .get(`/custom-sounds/${fileId}.wav`) .set(credentials) .set({ diff --git a/apps/meteor/tests/end-to-end/api/17-custom-user-status.js b/apps/meteor/tests/end-to-end/api/17-custom-user-status.ts similarity index 91% rename from apps/meteor/tests/end-to-end/api/17-custom-user-status.js rename to apps/meteor/tests/end-to-end/api/17-custom-user-status.ts index 711d037a9b981..c1ecd185dae83 100644 --- a/apps/meteor/tests/end-to-end/api/17-custom-user-status.js +++ b/apps/meteor/tests/end-to-end/api/17-custom-user-status.ts @@ -1,16 +1,14 @@ import { expect } from 'chai'; import { before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; - -describe('[CustomUserStatus]', function () { - this.retries(0); +import { getCredentials, api, request, credentials } from '../../data/api-data'; +describe('[CustomUserStatus]', () => { before((done) => getCredentials(done)); describe('[/custom-user-status.list]', () => { it('should return custom user status', (done) => { - request + void request .get(api('custom-user-status.list')) .set(credentials) .expect(200) @@ -23,7 +21,7 @@ describe('[CustomUserStatus]', function () { .end(done); }); it('should return custom user status even requested with count and offset params', (done) => { - request + void request .get(api('custom-user-status.list')) .set(credentials) .expect(200) diff --git a/apps/meteor/tests/end-to-end/api/17-webdav.js b/apps/meteor/tests/end-to-end/api/17-webdav.ts similarity index 92% rename from apps/meteor/tests/end-to-end/api/17-webdav.js rename to apps/meteor/tests/end-to-end/api/17-webdav.ts index c4348d6112f12..1dc2957fd6baf 100644 --- a/apps/meteor/tests/end-to-end/api/17-webdav.js +++ b/apps/meteor/tests/end-to-end/api/17-webdav.ts @@ -1,16 +1,14 @@ import { expect } from 'chai'; import { before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; - -describe('[Webdav]', function () { - this.retries(0); +import { getCredentials, api, request, credentials } from '../../data/api-data'; +describe('[Webdav]', () => { before((done) => getCredentials(done)); describe('/webdav.getMyAccounts', () => { it('should return my webdav accounts', (done) => { - request + void request .get(api('webdav.getMyAccounts')) .set(credentials) .expect(200) @@ -24,7 +22,7 @@ describe('[Webdav]', function () { describe('/webdav.removeWebdavAccount', () => { it('should return an error when send an invalid request', (done) => { - request + void request .post(api('webdav.removeWebdavAccount')) .set(credentials) .send({}) @@ -36,7 +34,7 @@ describe('[Webdav]', function () { .end(done); }); it('should return an error when using an invalid account id', (done) => { - request + void request .post(api('webdav.removeWebdavAccount')) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/18-oauthapps.js b/apps/meteor/tests/end-to-end/api/18-oauthapps.ts similarity index 88% rename from apps/meteor/tests/end-to-end/api/18-oauthapps.js rename to apps/meteor/tests/end-to-end/api/18-oauthapps.ts index 4566dab47967a..7bffa3297bfc7 100644 --- a/apps/meteor/tests/end-to-end/api/18-oauthapps.js +++ b/apps/meteor/tests/end-to-end/api/18-oauthapps.ts @@ -1,12 +1,12 @@ +import type { IOAuthApps } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updatePermission } from '../../data/permissions.helper'; -describe('[OAuthApps]', function () { - const createdAppsIds = []; - this.retries(0); +describe('[OAuthApps]', () => { + const createdAppsIds: IOAuthApps['_id'][] = []; before((done) => getCredentials(done)); @@ -23,8 +23,8 @@ describe('[OAuthApps]', function () { describe('[/oauth-apps.list]', () => { it('should return an error when the user does not have the necessary permission', (done) => { - updatePermission('manage-oauth-apps', []).then(() => { - request + void updatePermission('manage-oauth-apps', []).then(() => { + void request .get(api('oauth-apps.list')) .set(credentials) .expect(400) @@ -36,8 +36,8 @@ describe('[OAuthApps]', function () { }); }); it('should return an array of oauth apps', (done) => { - updatePermission('manage-oauth-apps', ['admin']).then(() => { - request + void updatePermission('manage-oauth-apps', ['admin']).then(() => { + void request .get(api('oauth-apps.list')) .set(credentials) .expect(200) @@ -52,8 +52,9 @@ describe('[OAuthApps]', function () { describe('[/oauth-apps.get]', () => { it('should return a single oauthApp by id', (done) => { - request - .get(api('oauth-apps.get?appId=zapier')) + void request + .get(api('oauth-apps.get')) + .query({ appId: 'zapier' }) .set(credentials) .expect(200) .expect((res) => { @@ -64,8 +65,9 @@ describe('[OAuthApps]', function () { .end(done); }); it('should return a single oauthApp by client id', (done) => { - request - .get(api('oauth-apps.get?clientId=zapier')) + void request + .get(api('oauth-apps.get')) + .query({ clientId: 'zapier' }) .set(credentials) .expect(200) .expect((res) => { @@ -76,9 +78,10 @@ describe('[OAuthApps]', function () { .end(done); }); it('should return a 403 Forbidden error when the user does not have the necessary permission by client id', (done) => { - updatePermission('manage-oauth-apps', []).then(() => { - request - .get(api('oauth-apps.get?clientId=zapier')) + void updatePermission('manage-oauth-apps', []).then(() => { + void request + .get(api('oauth-apps.get')) + .query({ clientId: 'zapier' }) .set(credentials) .expect(403) .expect((res) => { @@ -89,9 +92,10 @@ describe('[OAuthApps]', function () { }); }); it('should return a 403 Forbidden error when the user does not have the necessary permission by app id', (done) => { - updatePermission('manage-oauth-apps', []).then(() => { - request - .get(api('oauth-apps.get?appId=zapier')) + void updatePermission('manage-oauth-apps', []).then(() => { + void request + .get(api('oauth-apps.get')) + .query({ appId: 'zapier' }) .set(credentials) .expect(403) .expect((res) => { @@ -197,13 +201,13 @@ describe('[OAuthApps]', function () { }); describe('[/oauth-apps.update]', () => { - let appId; + let appId: IOAuthApps['_id']; before((done) => { const name = 'test-oauth-app'; const redirectUri = 'https://test.com'; const active = true; - request + void request .post(api('oauth-apps.create')) .set(credentials) .send({ @@ -213,7 +217,7 @@ describe('[OAuthApps]', function () { }) .expect('Content-Type', 'application/json') .expect(200) - .end((err, res) => { + .end((_err, res) => { appId = res.body.application._id; createdAppsIds.push(appId); done(); @@ -246,13 +250,13 @@ describe('[OAuthApps]', function () { }); describe('[/oauth-apps.delete]', () => { - let appId; + let appId: IOAuthApps['_id']; before((done) => { const name = 'test-oauth-app'; const redirectUri = 'https://test.com'; const active = true; - request + void request .post(api('oauth-apps.create')) .set(credentials) .send({ @@ -262,7 +266,7 @@ describe('[OAuthApps]', function () { }) .expect('Content-Type', 'application/json') .expect(200) - .end((err, res) => { + .end((_err, res) => { appId = res.body.application._id; done(); }); diff --git a/apps/meteor/tests/end-to-end/api/19-statistics.js b/apps/meteor/tests/end-to-end/api/19-statistics.ts similarity index 84% rename from apps/meteor/tests/end-to-end/api/19-statistics.js rename to apps/meteor/tests/end-to-end/api/19-statistics.ts index 4e1f0d8b0148b..c10eff9298075 100644 --- a/apps/meteor/tests/end-to-end/api/19-statistics.js +++ b/apps/meteor/tests/end-to-end/api/19-statistics.ts @@ -1,21 +1,19 @@ import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updatePermission } from '../../data/permissions.helper'; -describe('[Statistics]', function () { - this.retries(0); - +describe('[Statistics]', () => { before((done) => getCredentials(done)); after(() => updatePermission('view-statistics', ['admin'])); describe('[/statistics]', () => { - let lastUptime; + let lastUptime: unknown; it('should return an error when the user does not have the necessary permission', (done) => { - updatePermission('view-statistics', []).then(() => { - request + void updatePermission('view-statistics', []).then(() => { + void request .get(api('statistics')) .set(credentials) .expect(400) @@ -27,8 +25,8 @@ describe('[Statistics]', function () { }); }); it('should return an object with the statistics', (done) => { - updatePermission('view-statistics', ['admin']).then(() => { - request + void updatePermission('view-statistics', ['admin']).then(() => { + void request .get(api('statistics')) .set(credentials) .expect(200) @@ -42,8 +40,9 @@ describe('[Statistics]', function () { }); }); it('should update the statistics when is provided the "refresh:true" query parameter', (done) => { - request - .get(api('statistics?refresh=true')) + void request + .get(api('statistics')) + .query({ refresh: 'true' }) .set(credentials) .expect(200) .expect((res) => { @@ -58,8 +57,8 @@ describe('[Statistics]', function () { describe('[/statistics.list]', () => { it('should return an error when the user does not have the necessary permission', (done) => { - updatePermission('view-statistics', []).then(() => { - request + void updatePermission('view-statistics', []).then(() => { + void request .get(api('statistics.list')) .set(credentials) .expect(400) @@ -71,8 +70,8 @@ describe('[Statistics]', function () { }); }); it('should return an array with the statistics', (done) => { - updatePermission('view-statistics', ['admin']).then(() => { - request + void updatePermission('view-statistics', ['admin']).then(() => { + void request .get(api('statistics.list')) .set(credentials) .expect(200) @@ -87,8 +86,8 @@ describe('[Statistics]', function () { }); }); it('should return an array with the statistics even requested with count and offset params', (done) => { - updatePermission('view-statistics', ['admin']).then(() => { - request + void updatePermission('view-statistics', ['admin']).then(() => { + void request .get(api('statistics.list')) .set(credentials) .query({ diff --git a/apps/meteor/tests/end-to-end/api/20-licenses.js b/apps/meteor/tests/end-to-end/api/20-licenses.ts similarity index 91% rename from apps/meteor/tests/end-to-end/api/20-licenses.js rename to apps/meteor/tests/end-to-end/api/20-licenses.ts index 83867712a80d3..7792d497fe1b8 100644 --- a/apps/meteor/tests/end-to-end/api/20-licenses.js +++ b/apps/meteor/tests/end-to-end/api/20-licenses.ts @@ -1,16 +1,18 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('licenses', function () { - let createdUser; - this.retries(0); +describe('licenses', () => { + let createdUser: TestUser; before((done) => getCredentials(done)); - let unauthorizedUserCredentials; + let unauthorizedUserCredentials: Credentials; before(async () => { createdUser = await createUser(); @@ -21,7 +23,7 @@ describe('licenses', function () { describe('[/licenses.add]', () => { it('should fail if not logged in', (done) => { - request + void request .post(api('licenses.add')) .send({ license: '', @@ -36,7 +38,7 @@ describe('licenses', function () { }); it('should fail if user is unauthorized', (done) => { - request + void request .post(api('licenses.add')) .set(unauthorizedUserCredentials) .send({ @@ -52,7 +54,7 @@ describe('licenses', function () { }); it('should fail if license is invalid', (done) => { - request + void request .post(api('licenses.add')) .set(credentials) .send({ @@ -70,7 +72,7 @@ describe('licenses', function () { describe('[/licenses.get]', () => { it('should fail if not logged in', (done) => { - request + void request .get(api('licenses.get')) .expect('Content-Type', 'application/json') .expect(401) @@ -82,7 +84,7 @@ describe('licenses', function () { }); it('should fail if user is unauthorized', (done) => { - request + void request .get(api('licenses.get')) .set(unauthorizedUserCredentials) .expect('Content-Type', 'application/json') @@ -95,7 +97,7 @@ describe('licenses', function () { }); it('should return licenses if user is logged in and is authorized', (done) => { - request + void request .get(api('licenses.get')) .set(credentials) .expect(200) @@ -110,7 +112,7 @@ describe('licenses', function () { describe('[/licenses.info]', () => { it('should fail if not logged in', (done) => { - request + void request .get(api('licenses.info')) .expect('Content-Type', 'application/json') .expect(401) @@ -122,7 +124,7 @@ describe('licenses', function () { }); it('should return limited information if user is unauthorized', (done) => { - request + void request .get(api('licenses.info')) .set(unauthorizedUserCredentials) .expect('Content-Type', 'application/json') @@ -137,7 +139,7 @@ describe('licenses', function () { }); it('should return unrestricted info if user is logged in and is authorized', (done) => { - request + void request .get(api('licenses.info')) .set(credentials) .expect(200) @@ -156,7 +158,7 @@ describe('licenses', function () { describe('[/licenses.isEnterprise]', () => { it('should fail if not logged in', (done) => { - request + void request .get(api('licenses.isEnterprise')) .expect('Content-Type', 'application/json') .expect(401) @@ -168,7 +170,7 @@ describe('licenses', function () { }); it('should pass if user has user role', (done) => { - request + void request .get(api('licenses.isEnterprise')) .set(unauthorizedUserCredentials) .expect('Content-Type', 'application/json') @@ -180,7 +182,7 @@ describe('licenses', function () { }); it('should pass if user has admin role', (done) => { - request + void request .get(api('licenses.isEnterprise')) .set(credentials) .expect('Content-Type', 'application/json') diff --git a/apps/meteor/tests/end-to-end/api/21-banners.js b/apps/meteor/tests/end-to-end/api/21-banners.ts similarity index 94% rename from apps/meteor/tests/end-to-end/api/21-banners.js rename to apps/meteor/tests/end-to-end/api/21-banners.ts index c206901d57ebd..a4fd2638e7ddf 100644 --- a/apps/meteor/tests/end-to-end/api/21-banners.js +++ b/apps/meteor/tests/end-to-end/api/21-banners.ts @@ -1,16 +1,14 @@ import { expect } from 'chai'; import { before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; - -describe('banners', function () { - this.retries(0); +import { getCredentials, api, request, credentials } from '../../data/api-data'; +describe('banners', () => { before((done) => getCredentials(done)); describe('[/banners.getNew]', () => { it('should fail if not logged in', (done) => { - request + void request .get(api('banners.getNew')) .query({ platform: 'web', @@ -24,7 +22,7 @@ describe('banners', function () { }); it('should fail if missing platform key', (done) => { - request + void request .get(api('banners.getNew')) .set(credentials) .expect(400) @@ -35,7 +33,7 @@ describe('banners', function () { }); it('should fail if platform param is unknown', (done) => { - request + void request .get(api('banners.getNew')) .set(credentials) .query({ @@ -49,7 +47,7 @@ describe('banners', function () { }); it('should fail if platform param is empty', (done) => { - request + void request .get(api('banners.getNew')) .set(credentials) .query({ @@ -63,7 +61,7 @@ describe('banners', function () { }); it('should return banners if platform param is valid', (done) => { - request + void request .get(api('banners.getNew')) .set(credentials) .query({ @@ -80,7 +78,7 @@ describe('banners', function () { describe('[/banners.dismiss]', () => { it('should fail if not logged in', (done) => { - request + void request .post(api('banners.dismiss')) .send({ bannerId: '123', @@ -94,7 +92,7 @@ describe('banners', function () { }); it('should fail if missing bannerId key', (done) => { - request + void request .post(api('banners.dismiss')) .set(credentials) .send({}) @@ -107,7 +105,7 @@ describe('banners', function () { }); it('should fail if missing bannerId is empty', (done) => { - request + void request .post(api('banners.dismiss')) .set(credentials) .send({ @@ -121,7 +119,7 @@ describe('banners', function () { }); it('should fail if missing bannerId is invalid', (done) => { - request + void request .post(api('banners.dismiss')) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/22-push.ts b/apps/meteor/tests/end-to-end/api/22-push.ts index 7035260816d90..f496d0dfe9e9e 100644 --- a/apps/meteor/tests/end-to-end/api/22-push.ts +++ b/apps/meteor/tests/end-to-end/api/22-push.ts @@ -1,12 +1,10 @@ import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updateSetting } from '../../data/permissions.helper'; -describe('[Push]', function () { - this.retries(0); - +describe('[Push]', () => { before((done) => getCredentials(done)); describe('POST [/push.token]', () => { diff --git a/apps/meteor/tests/end-to-end/api/23-invites.js b/apps/meteor/tests/end-to-end/api/23-invites.ts similarity index 93% rename from apps/meteor/tests/end-to-end/api/23-invites.js rename to apps/meteor/tests/end-to-end/api/23-invites.ts index ef6238f4fc731..85f0771f67d13 100644 --- a/apps/meteor/tests/end-to-end/api/23-invites.js +++ b/apps/meteor/tests/end-to-end/api/23-invites.ts @@ -1,16 +1,16 @@ +import type { IInvite } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; -describe('Invites', function () { - let testInviteID; - this.retries(0); +describe('Invites', () => { + let testInviteID: IInvite['_id']; before((done) => getCredentials(done)); describe('POST [/findOrCreateInvite]', () => { it('should fail if not logged in', (done) => { - request + void request .post(api('findOrCreateInvite')) .send({ rid: 'GENERAL', @@ -26,7 +26,7 @@ describe('Invites', function () { }); it('should fail if invalid roomid', (done) => { - request + void request .post(api('findOrCreateInvite')) .set(credentials) .send({ @@ -43,7 +43,7 @@ describe('Invites', function () { }); it('should create an invite for GENERAL', (done) => { - request + void request .post(api('findOrCreateInvite')) .set(credentials) .send({ @@ -64,7 +64,7 @@ describe('Invites', function () { }); it('should return an existing invite for GENERAL', (done) => { - request + void request .post(api('findOrCreateInvite')) .set(credentials) .send({ @@ -86,7 +86,7 @@ describe('Invites', function () { describe('GET [/listInvites]', () => { it('should fail if not logged in', (done) => { - request + void request .get(api('listInvites')) .expect(401) .expect((res) => { @@ -97,7 +97,7 @@ describe('Invites', function () { }); it('should return the existing invite for GENERAL', (done) => { - request + void request .get(api('listInvites')) .set(credentials) .expect(200) @@ -110,7 +110,7 @@ describe('Invites', function () { describe('POST [/useInviteToken]', () => { it('should fail if not logged in', (done) => { - request + void request .post(api('useInviteToken')) .expect(401) .expect((res) => { @@ -121,7 +121,7 @@ describe('Invites', function () { }); it('should fail if invalid token', (done) => { - request + void request .post(api('useInviteToken')) .set(credentials) .send({ @@ -136,7 +136,7 @@ describe('Invites', function () { }); it('should fail if missing token', (done) => { - request + void request .post(api('useInviteToken')) .set(credentials) .send({}) @@ -149,7 +149,7 @@ describe('Invites', function () { }); it('should use the existing invite for GENERAL', (done) => { - request + void request .post(api('useInviteToken')) .set(credentials) .send({ @@ -165,7 +165,7 @@ describe('Invites', function () { describe('POST [/validateInviteToken]', () => { it('should warn if invalid token', (done) => { - request + void request .post(api('validateInviteToken')) .set(credentials) .send({ @@ -180,7 +180,7 @@ describe('Invites', function () { }); it('should succeed when valid token', (done) => { - request + void request .post(api('validateInviteToken')) .set(credentials) .send({ @@ -197,7 +197,7 @@ describe('Invites', function () { describe('DELETE [/removeInvite]', () => { it('should fail if not logged in', (done) => { - request + void request .delete(api(`removeInvite/${testInviteID}`)) .expect(401) .expect((res) => { @@ -208,7 +208,7 @@ describe('Invites', function () { }); it('should fail if invalid token', (done) => { - request + void request .delete(api('removeInvite/invalid')) .set(credentials) .expect(400) @@ -220,7 +220,7 @@ describe('Invites', function () { }); it('should succeed when valid token', (done) => { - request + void request .delete(api(`removeInvite/${testInviteID}`)) .set(credentials) .expect(200) @@ -231,7 +231,7 @@ describe('Invites', function () { }); it('should fail when deleting the same invite again', (done) => { - request + void request .delete(api(`removeInvite/${testInviteID}`)) .set(credentials) .expect(400) diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.ts similarity index 95% rename from apps/meteor/tests/end-to-end/api/24-methods.js rename to apps/meteor/tests/end-to-end/api/24-methods.ts index dc1d506f13179..d03b83854b28a 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.ts @@ -1,29 +1,30 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IMessage, IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, beforeEach, describe, it } from 'mocha'; -import { api, credentials, getCredentials, methodCall, request } from '../../data/api-data.js'; -import { sendSimpleMessage } from '../../data/chat.helper.js'; +import { api, credentials, getCredentials, methodCall, request } from '../../data/api-data'; +import { sendSimpleMessage } from '../../data/chat.helper'; import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; -import { createUser, deleteUser, login } from '../../data/users.helper.js'; +import type { TestUser } from '../../data/users.helper'; +import { createUser, deleteUser, login } from '../../data/users.helper'; import { IS_EE } from '../../e2e/config/constants'; -describe('Meteor.methods', function () { - this.retries(0); - +describe('Meteor.methods', () => { before((done) => getCredentials(done)); describe('[@getThreadMessages]', () => { - let rid = false; - let firstMessage = false; + let rid: IRoom['_id']; + let firstMessage: IMessage; - let channelName = false; + let channelName: string; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -43,7 +44,7 @@ describe('Meteor.methods', function () { }); before('send sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -62,7 +63,7 @@ describe('Meteor.methods', function () { }); before('send sample message into thread', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -83,7 +84,7 @@ describe('Meteor.methods', function () { after(() => deleteRoom({ type: 'p', roomId: rid })); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('getThreadMessages')) .send({ message: JSON.stringify({ @@ -103,7 +104,7 @@ describe('Meteor.methods', function () { }); it('should return message thread', (done) => { - request + void request .post(methodCall('getThreadMessages')) .set(credentials) .send({ @@ -174,11 +175,11 @@ describe('Meteor.methods', function () { }); (IS_EE ? describe : describe.skip)('[@getReadReceipts] EE', () => { - let user = null; - let userCredentials = null; - let room = null; - let firstMessage = null; - let firstThreadMessage = null; + let user: TestUser; + let userCredentials: Credentials; + let room: IRoom; + let firstMessage: IMessage; + let firstThreadMessage: IThreadMessage; const roomName = `methods-test-channel-${Date.now()}`; before(async () => { @@ -368,8 +369,8 @@ describe('Meteor.methods', function () { }); describe('simple message and thread marked as read by the invited user', () => { - let otherMessage = null; - let otherThreadMessage = null; + let otherMessage: IMessage; + let otherThreadMessage: IThreadMessage; before('should send another message and create a thread', async () => { otherMessage = (await sendSimpleMessage({ roomId: room._id })).body.message; @@ -448,15 +449,15 @@ describe('Meteor.methods', function () { }); describe('[@getMessages]', () => { - let rid = false; - let firstMessage = false; - let lastMessage = false; + let rid: IRoom['_id']; + let firstMessage: IMessage; + let lastMessage: IMessage; - let channelName = false; + let channelName: string; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -476,7 +477,7 @@ describe('Meteor.methods', function () { }); before('send sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -495,7 +496,7 @@ describe('Meteor.methods', function () { }); before('send another sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -516,7 +517,7 @@ describe('Meteor.methods', function () { after(() => deleteRoom({ type: 'p', roomId: rid })); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('getMessages')) .send({ message: JSON.stringify({ @@ -536,7 +537,7 @@ describe('Meteor.methods', function () { }); it('should fail if msgIds not specified', (done) => { - request + void request .post(methodCall('getMessages')) .set(credentials) .send({ @@ -562,7 +563,7 @@ describe('Meteor.methods', function () { }); it('should return the first message', (done) => { - request + void request .post(methodCall('getMessages')) .set(credentials) .send({ @@ -587,7 +588,7 @@ describe('Meteor.methods', function () { }); it('should return both messages', (done) => { - request + void request .post(methodCall('getMessages')) .set(credentials) .send({ @@ -613,13 +614,13 @@ describe('Meteor.methods', function () { }); describe('[@cleanRoomHistory]', () => { - let rid = false; + let rid: IRoom['_id']; - let channelName = false; + let channelName: string; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -639,7 +640,7 @@ describe('Meteor.methods', function () { }); before('send sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -657,7 +658,7 @@ describe('Meteor.methods', function () { }); before('send another sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -736,15 +737,15 @@ describe('Meteor.methods', function () { }); describe('[@loadHistory]', () => { - let rid = false; - let postMessageDate = false; - let lastMessage = false; + let rid: IRoom['_id']; + let postMessageDate: unknown; + let lastMessage: IMessage; - let channelName = false; + let channelName: string; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -764,7 +765,7 @@ describe('Meteor.methods', function () { }); before('send sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -783,7 +784,7 @@ describe('Meteor.methods', function () { }); before('send another sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -804,7 +805,7 @@ describe('Meteor.methods', function () { after(() => deleteRoom({ type: 'p', roomId: rid })); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('loadHistory')) .send({ message: JSON.stringify({ @@ -824,7 +825,7 @@ describe('Meteor.methods', function () { }); it('should fail if roomId not specified', (done) => { - request + void request .post(methodCall('loadHistory')) .set(credentials) .send({ @@ -850,7 +851,7 @@ describe('Meteor.methods', function () { }); it('should return all messages for the specified room', (done) => { - request + void request .post(methodCall('loadHistory')) .set(credentials) .send({ @@ -876,7 +877,7 @@ describe('Meteor.methods', function () { }); it('should return only the first message', (done) => { - request + void request .post(methodCall('loadHistory')) .set(credentials) .send({ @@ -902,7 +903,7 @@ describe('Meteor.methods', function () { }); it('should return only one message when limit = 1', (done) => { - request + void request .post(methodCall('loadHistory')) .set(credentials) .send({ @@ -928,7 +929,7 @@ describe('Meteor.methods', function () { }); it('should return the messages since the last one', (done) => { - request + void request .post(methodCall('loadHistory')) .set(credentials) .send({ @@ -955,15 +956,15 @@ describe('Meteor.methods', function () { }); describe('[@loadNextMessages]', () => { - let rid = false; - let postMessageDate = false; + let rid: IRoom['_id']; + let postMessageDate: unknown; const startDate = { $date: new Date().getTime() }; - let channelName = false; + let channelName: string; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -983,7 +984,7 @@ describe('Meteor.methods', function () { }); before('send sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1002,7 +1003,7 @@ describe('Meteor.methods', function () { }); before('send another sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1022,7 +1023,7 @@ describe('Meteor.methods', function () { after(() => deleteRoom({ type: 'p', roomId: rid })); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('loadNextMessages')) .send({ message: JSON.stringify({ @@ -1042,7 +1043,7 @@ describe('Meteor.methods', function () { }); it('should fail if roomId not specified', (done) => { - request + void request .post(methodCall('loadNextMessages')) .set(credentials) .send({ @@ -1068,7 +1069,7 @@ describe('Meteor.methods', function () { }); it('should return all messages for the specified room', (done) => { - request + void request .post(methodCall('loadNextMessages')) .set(credentials) .send({ @@ -1094,7 +1095,7 @@ describe('Meteor.methods', function () { }); it('should return only the latest message', (done) => { - request + void request .post(methodCall('loadNextMessages')) .set(credentials) .send({ @@ -1120,7 +1121,7 @@ describe('Meteor.methods', function () { }); it('should return only one message when limit = 1', (done) => { - request + void request .post(methodCall('loadNextMessages')) .set(credentials) .send({ @@ -1147,14 +1148,14 @@ describe('Meteor.methods', function () { }); describe('[@getUsersOfRoom]', () => { - let testUser; - let rid = false; + let testUser: TestUser; + let rid: IRoom['_id']; - let channelName = false; + let channelName: string; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -1176,18 +1177,18 @@ describe('Meteor.methods', function () { before('create test user', (done) => { const username = `user.test.${Date.now()}`; const email = `${username}@rocket.chat`; - request + void request .post(api('users.create')) .set(credentials) .send({ email, name: username, username, password: username }) - .end((err, res) => { + .end((_err, res) => { testUser = res.body.user; done(); }); }); before('add user to room', (done) => { - request + void request .post(api('groups.invite')) .set(credentials) .send({ @@ -1202,7 +1203,7 @@ describe('Meteor.methods', function () { after(() => Promise.all([deleteRoom({ type: 'p', roomId: rid }), deleteUser(testUser)])); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('getUsersOfRoom')) .send({ message: JSON.stringify({ @@ -1222,7 +1223,7 @@ describe('Meteor.methods', function () { }); it('should fail if roomId not specified', (done) => { - request + void request .post(methodCall('getUsersOfRoom')) .set(credentials) .send({ @@ -1247,7 +1248,7 @@ describe('Meteor.methods', function () { }); it('should return the users for the specified room', (done) => { - request + void request .post(methodCall('getUsersOfRoom')) .set(credentials) .send({ @@ -1274,7 +1275,7 @@ describe('Meteor.methods', function () { describe('[@getUserRoles]', () => { it('should fail if not logged in', (done) => { - request + void request .post(methodCall('getUserRoles')) .send({ message: JSON.stringify({ @@ -1294,7 +1295,7 @@ describe('Meteor.methods', function () { }); it('should return the roles for the current user', (done) => { - request + void request .post(methodCall('getUserRoles')) .set(credentials) .send({ @@ -1320,7 +1321,7 @@ describe('Meteor.methods', function () { describe('[@listCustomUserStatus]', () => { it('should fail if not logged in', (done) => { - request + void request .post(methodCall('listCustomUserStatus')) .send({ message: JSON.stringify({ @@ -1340,7 +1341,7 @@ describe('Meteor.methods', function () { }); it('should return custom status for the current user', (done) => { - request + void request .post(methodCall('listCustomUserStatus')) .set(credentials) .send({ @@ -1370,7 +1371,7 @@ describe('Meteor.methods', function () { }; it('should fail if not logged in', (done) => { - request + void request .post(methodCall('permissions:get')) .send({ message: JSON.stringify({ @@ -1390,7 +1391,7 @@ describe('Meteor.methods', function () { }); it('should return all permissions', (done) => { - request + void request .post(methodCall('permissions:get')) .set(credentials) .send({ @@ -1415,7 +1416,7 @@ describe('Meteor.methods', function () { }); it('should return all permissions after the given date', (done) => { - request + void request .post(methodCall('permissions:get')) .set(credentials) .send({ @@ -1441,16 +1442,16 @@ describe('Meteor.methods', function () { }); describe('[@loadMissedMessages]', () => { - let rid = false; + let rid: IRoom['_id']; const date = { $date: new Date().getTime(), }; - let postMessageDate = false; + let postMessageDate: unknown; const channelName = `methods-test-channel-${Date.now()}`; before('create test group', (done) => { - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -1470,7 +1471,7 @@ describe('Meteor.methods', function () { }); before('send sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1489,7 +1490,7 @@ describe('Meteor.methods', function () { }); before('send another sample message', (done) => { - request + void request .post(api('chat.sendMessage')) .set(credentials) .send({ @@ -1509,7 +1510,7 @@ describe('Meteor.methods', function () { after(() => deleteRoom({ type: 'p', roomId: rid })); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('loadMissedMessages')) .send({ message: JSON.stringify({ @@ -1529,7 +1530,7 @@ describe('Meteor.methods', function () { }); it('should return an error if the rid param is empty', (done) => { - request + void request .post(methodCall('loadMissedMessages')) .set(credentials) .send({ @@ -1550,7 +1551,7 @@ describe('Meteor.methods', function () { }); it('should return an error if the start param is missing', (done) => { - request + void request .post(methodCall('loadMissedMessages')) .set(credentials) .send({ @@ -1571,7 +1572,7 @@ describe('Meteor.methods', function () { }); it('should return and empty list if using current time', (done) => { - request + void request .post(methodCall('loadMissedMessages')) .set(credentials) .send({ @@ -1596,7 +1597,7 @@ describe('Meteor.methods', function () { }); it('should return two messages if using a time from before the first msg was sent', (done) => { - request + void request .post(methodCall('loadMissedMessages')) .set(credentials) .send({ @@ -1621,7 +1622,7 @@ describe('Meteor.methods', function () { }); it('should return a single message if using a time from in between the messages', (done) => { - request + void request .post(methodCall('loadMissedMessages')) .set(credentials) .send({ @@ -1652,7 +1653,7 @@ describe('Meteor.methods', function () { }; it('should fail if not logged in', (done) => { - request + void request .post(methodCall('public-settings:get')) .send({ message: JSON.stringify({ @@ -1672,7 +1673,7 @@ describe('Meteor.methods', function () { }); it('should return the list of public settings', (done) => { - request + void request .post(methodCall('public-settings:get')) .set(credentials) .send({ @@ -1710,7 +1711,7 @@ describe('Meteor.methods', function () { ); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('private-settings:get')) .send({ message: JSON.stringify({ @@ -1730,11 +1731,11 @@ describe('Meteor.methods', function () { }); it('should return nothing when user doesnt have any permission', (done) => { - updatePermission('view-privileged-setting', []) - .then(updatePermission('edit-privileged-setting', [])) - .then(updatePermission('manage-selected-settings', [])) + void updatePermission('view-privileged-setting', []) + .then(() => updatePermission('edit-privileged-setting', [])) + .then(() => updatePermission('manage-selected-settings', [])) .then(() => { - request + void request .post(methodCall('private-settings:get')) .set(credentials) .send({ @@ -1760,8 +1761,8 @@ describe('Meteor.methods', function () { }); it('should return properties when user has any related permissions', (done) => { - updatePermission('view-privileged-setting', ['admin']).then(() => { - request + void updatePermission('view-privileged-setting', ['admin']).then(() => { + void request .post(methodCall('private-settings:get')) .set(credentials) .send({ @@ -1788,11 +1789,11 @@ describe('Meteor.methods', function () { }); it('should return properties when user has all related permissions', (done) => { - updatePermission('view-privileged-setting', ['admin']) - .then(updatePermission('edit-privileged-setting', ['admin'])) - .then(updatePermission('manage-selected-settings', ['admin'])) + void updatePermission('view-privileged-setting', ['admin']) + .then(() => updatePermission('edit-privileged-setting', ['admin'])) + .then(() => updatePermission('manage-selected-settings', ['admin'])) .then(() => { - request + void request .post(methodCall('private-settings:get')) .set(credentials) .send({ @@ -1825,7 +1826,7 @@ describe('Meteor.methods', function () { }; it('should fail if not logged in', (done) => { - request + void request .post(methodCall('subscriptions:get')) .send({ message: JSON.stringify({ @@ -1845,7 +1846,7 @@ describe('Meteor.methods', function () { }); it('should return all subscriptions', (done) => { - request + void request .post(methodCall('subscriptions:get')) .set(credentials) .send({ @@ -1870,7 +1871,7 @@ describe('Meteor.methods', function () { }); it('should return all subscriptions after the given date', (done) => { - request + void request .post(methodCall('subscriptions:get')) .set(credentials) .send({ @@ -1896,12 +1897,12 @@ describe('Meteor.methods', function () { }); describe('[@sendMessage]', () => { - let rid = false; - let channelName = false; + let rid: IRoom['_id']; + let channelName: string; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -1923,7 +1924,7 @@ describe('Meteor.methods', function () { after(() => deleteRoom({ type: 'p', roomId: rid })); it('should send a message', (done) => { - request + void request .post(methodCall('sendMessage')) .set(credentials) .send({ @@ -1948,7 +1949,7 @@ describe('Meteor.methods', function () { }); it('should parse correctly urls sent in message', (done) => { - request + void request .post(methodCall('sendMessage')) .set(credentials) .send({ @@ -1981,17 +1982,17 @@ describe('Meteor.methods', function () { }); describe('[@updateMessage]', () => { - let rid = false; - let roomName = false; - let messageId; - let simpleMessageId; - let messageWithMarkdownId; - let channelName = false; + let rid: IRoom['_id']; + let roomName: string; + let messageId: IMessage['_id']; + let simpleMessageId: IMessage['_id']; + let messageWithMarkdownId: IMessage['_id']; + let channelName: string; const siteUrl = process.env.SITE_URL || process.env.TEST_API_URL || 'http://localhost:3000'; before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -2017,7 +2018,7 @@ describe('Meteor.methods', function () { }); before('send message with URL', (done) => { - request + void request .post(methodCall('sendMessage')) .set(credentials) .send({ @@ -2050,7 +2051,7 @@ describe('Meteor.methods', function () { }); before('send message with URL inside markdown', (done) => { - request + void request .post(methodCall('sendMessage')) .set(credentials) .send({ @@ -2089,7 +2090,7 @@ describe('Meteor.methods', function () { ); it('should update a message with a URL', (done) => { - request + void request .post(methodCall('updateMessage')) .set(credentials) .send({ @@ -2132,7 +2133,8 @@ describe('Meteor.methods', function () { }); await request - .get(api(`chat.getMessage?msgId=${messageId}`)) + .get(api('chat.getMessage')) + .query({ msgId: messageId }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2165,7 +2167,8 @@ describe('Meteor.methods', function () { }); await request - .get(api(`chat.getMessage?msgId=${messageId}`)) + .get(api('chat.getMessage')) + .query({ msgId: messageId }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2199,7 +2202,8 @@ describe('Meteor.methods', function () { }); await request - .get(api(`chat.getMessage?msgId=${messageId}`)) + .get(api('chat.getMessage')) + .query({ msgId: messageId }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2232,7 +2236,8 @@ describe('Meteor.methods', function () { }); await request - .get(api(`chat.getMessage?msgId=${messageId}`)) + .get(api('chat.getMessage')) + .query({ msgId: messageId }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2268,7 +2273,8 @@ describe('Meteor.methods', function () { }); await request - .get(api(`chat.getMessage?msgId=${messageId}`)) + .get(api('chat.getMessage')) + .query({ msgId: messageId }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2284,7 +2290,7 @@ describe('Meteor.methods', function () { }); it('should not parse URLs inside markdown on update', (done) => { - request + void request .post(methodCall('updateMessage')) .set(credentials) .send({ @@ -2311,8 +2317,9 @@ describe('Meteor.methods', function () { expect(data).to.have.a.property('msg').that.is.an('string'); }) .then(() => { - request - .get(api(`chat.getMessage?msgId=${messageWithMarkdownId}`)) + void request + .get(api('chat.getMessage')) + .query({ msgId: messageWithMarkdownId }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -2328,7 +2335,7 @@ describe('Meteor.methods', function () { ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg'].forEach((prop) => { it(`should allow to update a message changing property '${prop}'`, (done) => { - request + void request .post(methodCall('updateMessage')) .set(credentials) .send({ @@ -2353,7 +2360,7 @@ describe('Meteor.methods', function () { ['tmid', '_hidden', 'rid'].forEach((prop) => { it(`should fail to update a message changing invalid property '${prop}'`, (done) => { - request + void request .post(methodCall('updateMessage')) .set(credentials) .send({ @@ -2380,12 +2387,12 @@ describe('Meteor.methods', function () { }); describe('[@deleteMessage]', () => { - let rid = false; - let messageId; + let rid: IRoom['_id']; + let messageId: IMessage['_id']; before('create room', (done) => { const channelName = `methods-test-channel-${Date.now()}`; - request + void request .post(api('groups.create')) .set(credentials) .send({ @@ -2405,7 +2412,7 @@ describe('Meteor.methods', function () { }); beforeEach('send message with URL', (done) => { - request + void request .post(methodCall('sendMessage')) .set(credentials) .send({ @@ -2446,7 +2453,7 @@ describe('Meteor.methods', function () { ); it('should delete a message', (done) => { - request + void request .post(methodCall('deleteMessage')) .set(credentials) .send({ @@ -2504,11 +2511,11 @@ describe('Meteor.methods', function () { }); describe('[@setUserActiveStatus]', () => { - let testUser; - let testUser2; - let testUserCredentials; - let dmId; - let dmTestId; + let testUser: TestUser; + let testUser2: TestUser; + let testUserCredentials: Credentials; + let dmId: IRoom['_id']; + let dmTestId: IRoom['_id']; before(async () => { testUser = await createUser(); @@ -2517,7 +2524,7 @@ describe('Meteor.methods', function () { }); before('create direct conversation with user', (done) => { - request + void request .post(methodCall('createDirectMessage')) .set(credentials) .send({ @@ -2528,7 +2535,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { const result = JSON.parse(res.body.message); expect(result.result).to.be.an('object'); expect(result.result).to.have.property('rid').that.is.an('string'); @@ -2539,7 +2546,7 @@ describe('Meteor.methods', function () { }); before('create direct conversation between both users', (done) => { - request + void request .post(methodCall('createDirectMessage')) .set(testUserCredentials) .send({ @@ -2550,7 +2557,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { const result = JSON.parse(res.body.message); expect(result.result).to.be.an('object'); expect(result.result).to.have.property('rid').that.is.an('string'); @@ -2570,7 +2577,7 @@ describe('Meteor.methods', function () { ); it('should deactivate a user', (done) => { - request + void request .post(methodCall('setUserActiveStatus')) .set(credentials) .send({ @@ -2581,7 +2588,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body).to.have.property('success').that.is.an('boolean'); const result = JSON.parse(res.body.message); expect(result.result).to.be.equal(true); @@ -2590,7 +2597,7 @@ describe('Meteor.methods', function () { }); it('should deactivate another user', (done) => { - request + void request .post(methodCall('setUserActiveStatus')) .set(credentials) .send({ @@ -2601,7 +2608,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body).to.have.property('success').that.is.an('boolean'); const result = JSON.parse(res.body.message); expect(result.result).to.be.equal(true); @@ -2610,7 +2617,7 @@ describe('Meteor.methods', function () { }); it('should mark the direct conversation between admin=>testUser as readonly when user is deactivated', (done) => { - request + void request .post(methodCall('getRoomByTypeAndName')) .set(credentials) .send({ @@ -2621,7 +2628,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body.success).to.equal(true); const result = JSON.parse(res.body.message); expect(result.result.ro).to.equal(true); @@ -2630,7 +2637,7 @@ describe('Meteor.methods', function () { }); it('should activate a user', (done) => { - request + void request .post(methodCall('setUserActiveStatus')) .set(credentials) .send({ @@ -2641,7 +2648,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body).to.have.property('success').that.is.an('boolean'); const result = JSON.parse(res.body.message); expect(result.result).to.be.equal(true); @@ -2650,7 +2657,7 @@ describe('Meteor.methods', function () { }); it('should set readonly=false when user is activated (and the other side is also active)', (done) => { - request + void request .post(methodCall('getRoomByTypeAndName')) .set(credentials) .send({ @@ -2661,7 +2668,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body.success).to.equal(true); const result = JSON.parse(res.body.message); expect(result.result.ro).to.equal(false); @@ -2670,7 +2677,7 @@ describe('Meteor.methods', function () { }); it('should keep the direct conversation between testUser=>testUser2 as readonly when one of them is deactivated', (done) => { - request + void request .post(api('login')) .send({ user: testUser.username, @@ -2683,7 +2690,7 @@ describe('Meteor.methods', function () { testUserCredentials['X-User-Id'] = res.body.data.userId; }) .then(() => { - request + void request .post(methodCall('getRoomByTypeAndName')) .set(testUserCredentials) .send({ @@ -2694,7 +2701,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body.success).to.equal(true); const result = JSON.parse(res.body.message); expect(result.result.ro).to.equal(true); @@ -2705,7 +2712,7 @@ describe('Meteor.methods', function () { }); it('should activate another user', (done) => { - request + void request .post(methodCall('setUserActiveStatus')) .set(credentials) .send({ @@ -2716,7 +2723,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body).to.have.property('success').that.is.an('boolean'); const result = JSON.parse(res.body.message); expect(result.result).to.be.equal(true); @@ -2725,7 +2732,7 @@ describe('Meteor.methods', function () { }); it('should set readonly=false when both users are activated', (done) => { - request + void request .post(methodCall('getRoomByTypeAndName')) .set(testUserCredentials) .send({ @@ -2736,7 +2743,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body.success).to.equal(true); const result = JSON.parse(res.body.message); expect(result.result.ro).to.equal(false); @@ -2745,7 +2752,7 @@ describe('Meteor.methods', function () { }); it('should keep readonly=true when user is activated (and the other side is deactivated)', (done) => { - request + void request .post(methodCall('getRoomByTypeAndName')) .set(testUserCredentials) .send({ @@ -2756,7 +2763,7 @@ describe('Meteor.methods', function () { msg: 'method', }), }) - .end((err, res) => { + .end((_err, res) => { expect(res.body.success).to.equal(true); const result = JSON.parse(res.body.message); expect(result.result.ro).to.equal(false); @@ -2766,10 +2773,10 @@ describe('Meteor.methods', function () { }); describe('[@addUsersToRoom]', () => { - let guestUser; - let user; - let room; - let createdRooms = []; + let guestUser: TestUser; + let user: TestUser; + let room: IRoom; + let createdRooms: IRoom[] = []; before(async () => { guestUser = await createUser({ roles: ['guest'] }); @@ -2787,7 +2794,7 @@ describe('Meteor.methods', function () { ); it('should fail if not logged in', (done) => { - request + void request .post(methodCall('addUsersToRoom')) .expect('Content-Type', 'application/json') .expect(401) @@ -2799,7 +2806,7 @@ describe('Meteor.methods', function () { }); it('should add a single user to a room', (done) => { - request + void request .post(methodCall('addUsersToRoom')) .set(credentials) .send({ @@ -2816,7 +2823,7 @@ describe('Meteor.methods', function () { expect(res.body).to.have.property('success', true); }) .then(() => { - request + void request .get(api('channels.members')) .set(credentials) .query({ @@ -2852,7 +2859,7 @@ describe('Meteor.methods', function () { } createdRooms = [...createdRooms, ...(await Promise.all(promises)).map((res) => res.body.channel)]; - request + void request .post(methodCall('addUsersToRoom')) .set(credentials) .send({ @@ -2875,9 +2882,9 @@ describe('Meteor.methods', function () { }); describe('[@muteUserInRoom & @unmuteUserInRoom]', () => { - let rid = null; - let channelName = null; - let testUser = null; + let rid: IRoom['_id']; + let channelName: string; + let testUser: TestUser; let testUserCredentials = {}; before('create test user', async () => { @@ -3111,7 +3118,7 @@ describe('Meteor.methods', function () { describe('[@saveSettings]', () => { it('should return an error when trying to save a "NaN" value', () => { - request + void request .post(api('method.call/saveSettings')) .set(credentials) .send({ @@ -3132,7 +3139,7 @@ describe('Meteor.methods', function () { }); it('should return an error when trying to save a "Infinity" value', () => { - request + void request .post(api('method.call/saveSettings')) .set(credentials) .send({ @@ -3153,7 +3160,7 @@ describe('Meteor.methods', function () { }); it('should return an error when trying to save a "-Infinity" value', () => { - request + void request .post(api('method.call/saveSettings')) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/api/25-teams.js b/apps/meteor/tests/end-to-end/api/25-teams.ts similarity index 85% rename from apps/meteor/tests/end-to-end/api/25-teams.js rename to apps/meteor/tests/end-to-end/api/25-teams.ts index 6fc9899e403b3..751bb71686075 100644 --- a/apps/meteor/tests/end-to-end/api/25-teams.js +++ b/apps/meteor/tests/end-to-end/api/25-teams.ts @@ -1,3 +1,5 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IRole, IRoom, ITeam, IUser } from '@rocket.chat/core-typings'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; @@ -5,17 +7,43 @@ import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; -import { addMembers, createTeam, deleteTeam } from '../../data/teams.helper'; +import { createTeam, deleteTeam } from '../../data/teams.helper'; import { adminUsername, password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login } from '../../data/users.helper'; +interface IUserInfo { + _id: string; + username?: string; + name?: string; + status?: string; + settings?: Record; +} + +interface ITeamMemberInfo { + user: IUserInfo; + roles?: IRole['_id'][] | null; + createdBy: Omit; + createdAt: Date; +} + +const addMembers = async (credentials: Record, teamName: string, members: IUser['_id'][]): Promise => { + await request + .post(api('teams.addMembers')) + .set(credentials) + .send({ + teamName, + members: members.map((userId) => ({ userId, roles: ['member'] })), + }); +}; + describe('[Teams]', () => { before((done) => getCredentials(done)); describe('/teams.create', () => { const name = `test-team-create-${Date.now()}`; - const createdTeams = []; - let testUser; + const createdTeams: ITeam[] = []; + let testUser: TestUser; before(async () => { testUser = await createUser(); @@ -24,7 +52,7 @@ describe('[Teams]', () => { after(() => Promise.all([...createdTeams.map((team) => deleteTeam(credentials, team.name)), deleteUser(testUser)])); it('should create a public team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -43,7 +71,7 @@ describe('[Teams]', () => { }); it('should create a public team with a member', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -71,7 +99,7 @@ describe('[Teams]', () => { expect(response.body).to.have.property('members'); // remove admin user from members because it's added automatically as owner - const members = response.body.members.filter(({ user }) => user.username !== adminUsername); + const members = (response.body.members as ITeamMemberInfo[]).filter(({ user }) => user.username !== adminUsername); const [member] = members; expect(member.user.username).to.be.equal(testUser.username); @@ -82,7 +110,7 @@ describe('[Teams]', () => { }); it('should create private team with a defined owner', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -128,7 +156,7 @@ describe('[Teams]', () => { }); it('should throw an error if the team already exists', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -147,28 +175,28 @@ describe('[Teams]', () => { }); describe('/teams.convertToChannel', () => { - let testTeam; - let channelToEraseId; - let channelToKeepId; + let testTeam: ITeam; + let channelToEraseId: IRoom['_id']; + let channelToKeepId: IRoom['_id']; const teamName = `test-team-convert-to-channel-${Date.now()}`; const channelToEraseName = `${teamName}-channelToErase`; const channelToKeepName = `${teamName}-channelToKeep`; before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName, type: 1, }) - .end((err, res) => { + .end((_err, res) => { testTeam = res.body.team; done(); }); }); before('create channel (to erase after its team is converted to a channel)', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -188,7 +216,7 @@ describe('[Teams]', () => { }); before('add first channel to team', (done) => { - request + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -208,7 +236,7 @@ describe('[Teams]', () => { }); before('create channel (to keep after its team is converted to a channel)', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -228,7 +256,7 @@ describe('[Teams]', () => { }); before('add second channel to team', (done) => { - request + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -255,7 +283,7 @@ describe('[Teams]', () => { ); it('should convert the team to a channel, delete the specified room and move the other back to the workspace', (done) => { - request + void request .post(api('teams.convertToChannel')) .set(credentials) .send({ @@ -268,7 +296,7 @@ describe('[Teams]', () => { expect(res.body).to.have.property('success', true); }) .then(() => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -283,7 +311,7 @@ describe('[Teams]', () => { }); }) .then(() => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -299,7 +327,7 @@ describe('[Teams]', () => { }); }) .then(() => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -321,10 +349,10 @@ describe('[Teams]', () => { }); describe('/teams.addMembers', () => { - let testTeam; + let testTeam: ITeam; const teamName = `test-team-add-members-${Date.now()}`; - let testUser; - let testUser2; + let testUser: TestUser; + let testUser2: TestUser; before(async () => { testUser = await createUser(); @@ -332,14 +360,14 @@ describe('[Teams]', () => { }); before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam = res.body.team; done(); }); @@ -348,7 +376,7 @@ describe('[Teams]', () => { after(() => Promise.all([deleteUser(testUser), deleteUser(testUser2), deleteTeam(credentials, teamName)])); it('should add members to a public team', (done) => { - request + void request .post(api('teams.addMembers')) .set(credentials) .send({ @@ -369,44 +397,45 @@ describe('[Teams]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); }) - .then(() => - request - .get(api('teams.members')) - .set(credentials) - .query({ - teamName: testTeam.name, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((response) => { - expect(response.body).to.have.property('success', true); - expect(response.body).to.have.property('members'); - expect(response.body.members).to.have.length(3); - expect(response.body.members[1]).to.have.property('user'); - expect(response.body.members[1]).to.have.property('roles'); - expect(response.body.members[1]).to.have.property('createdBy'); - expect(response.body.members[1]).to.have.property('createdAt'); - - const members = response.body.members.map(({ user, roles }) => ({ - _id: user._id, - username: user.username, - name: user.name, - roles, - })); - - expect(members).to.own.deep.include({ - _id: testUser._id, - username: testUser.username, - name: testUser.name, - roles: ['member'], - }); - expect(members).to.own.deep.include({ - _id: testUser2._id, - username: testUser2.username, - name: testUser2.name, - roles: ['member'], - }); - }), + .then( + () => + void request + .get(api('teams.members')) + .set(credentials) + .query({ + teamName: testTeam.name, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((response) => { + expect(response.body).to.have.property('success', true); + expect(response.body).to.have.property('members'); + expect(response.body.members).to.have.length(3); + expect(response.body.members[1]).to.have.property('user'); + expect(response.body.members[1]).to.have.property('roles'); + expect(response.body.members[1]).to.have.property('createdBy'); + expect(response.body.members[1]).to.have.property('createdAt'); + + const members = (response.body.members as ITeamMemberInfo[]).map(({ user, roles }) => ({ + _id: user._id, + username: user.username, + name: user.name, + roles, + })); + + expect(members).to.deep.own.include({ + _id: testUser._id, + username: testUser.username, + name: testUser.name, + roles: ['member'], + }); + expect(members).to.deep.own.include({ + _id: testUser2._id, + username: testUser2.username, + name: testUser2.name, + roles: ['member'], + }); + }), ) .then(() => done()) .catch(done); @@ -414,10 +443,10 @@ describe('[Teams]', () => { }); describe('/teams.members', () => { - let testTeam; + let testTeam: ITeam; const teamName = `test-team-members-${Date.now()}`; - let testUser; - let testUser2; + let testUser: TestUser; + let testUser2: TestUser; before(async () => { testUser = await createUser(); @@ -425,21 +454,21 @@ describe('[Teams]', () => { }); before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam = res.body.team; done(); }); }); before('Add members to team', (done) => { - request + void request .post(api('teams.addMembers')) .set(credentials) .send({ @@ -461,7 +490,7 @@ describe('[Teams]', () => { after(() => Promise.all([deleteUser(testUser), deleteUser(testUser2), deleteTeam(credentials, teamName)])); it('should list all the members from a public team', (done) => { - request + void request .get(api('teams.members')) .set(credentials) .query({ @@ -494,7 +523,7 @@ describe('[Teams]', () => { describe('/teams.list', () => { const teamName = `test-team-list-${Date.now()}`; before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -507,7 +536,7 @@ describe('[Teams]', () => { after(() => deleteTeam(credentials, teamName)); it('should list all teams', (done) => { - request + void request .get(api('teams.list')) .set(credentials) .expect('Content-Type', 'application/json') @@ -519,27 +548,27 @@ describe('[Teams]', () => { expect(res.body).to.have.property('total'); expect(res.body).to.have.property('teams'); expect(res.body.teams.length).equal(1); - expect(res.body.teams[0]).to.include.property('_id'); - expect(res.body.teams[0]).to.include.property('_updatedAt'); - expect(res.body.teams[0]).to.include.property('name'); - expect(res.body.teams[0]).to.include.property('type'); - expect(res.body.teams[0]).to.include.property('roomId'); - expect(res.body.teams[0]).to.include.property('createdBy'); - expect(res.body.teams[0].createdBy).to.include.property('_id'); - expect(res.body.teams[0].createdBy).to.include.property('username'); - expect(res.body.teams[0]).to.include.property('createdAt'); - expect(res.body.teams[0]).to.include.property('rooms'); - expect(res.body.teams[0]).to.include.property('numberOfUsers'); + expect(res.body.teams[0]).to.have.property('_id'); + expect(res.body.teams[0]).to.have.property('_updatedAt'); + expect(res.body.teams[0]).to.have.property('name'); + expect(res.body.teams[0]).to.have.property('type'); + expect(res.body.teams[0]).to.have.property('roomId'); + expect(res.body.teams[0]).to.have.property('createdBy'); + expect(res.body.teams[0].createdBy).to.have.property('_id'); + expect(res.body.teams[0].createdBy).to.have.property('username'); + expect(res.body.teams[0]).to.have.property('createdAt'); + expect(res.body.teams[0]).to.have.property('rooms'); + expect(res.body.teams[0]).to.have.property('numberOfUsers'); }) .end(done); }); }); describe('/teams.updateMember', () => { - let testTeam; + let testTeam: ITeam; const teamName = `test-team-update-member-${Date.now()}`; - let testUser; - let testUser2; + let testUser: TestUser; + let testUser2: TestUser; before(async () => { testUser = await createUser(); @@ -547,20 +576,20 @@ describe('[Teams]', () => { }); before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam = res.body.team; done(); }); }); before('Add members to team', (done) => { - request + void request .post(api('teams.addMembers')) .set(credentials) .send({ @@ -582,7 +611,7 @@ describe('[Teams]', () => { after(() => Promise.all([deleteUser(testUser), deleteUser(testUser2), deleteTeam(credentials, teamName)])); it("should update member's data in a public team", (done) => { - request + void request .post(api('teams.updateMember')) .set(credentials) .send({ @@ -597,34 +626,35 @@ describe('[Teams]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); }) - .then(() => - request - .get(api('teams.members')) - .set(credentials) - .query({ - teamName: testTeam.name, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((response) => { - expect(response.body).to.have.property('success', true); - expect(response.body).to.have.property('members'); - expect(response.body.members).to.have.length(3); - - const members = response.body.members.map(({ user, roles }) => ({ - _id: user._id, - username: user.username, - name: user.name, - roles, - })); - - expect(members).to.own.deep.include({ - _id: testUser._id, - username: testUser.username, - name: testUser.name, - roles: ['member', 'owner'], - }); - }), + .then( + () => + void request + .get(api('teams.members')) + .set(credentials) + .query({ + teamName: testTeam.name, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((response) => { + expect(response.body).to.have.property('success', true); + expect(response.body).to.have.property('members'); + expect(response.body.members).to.have.length(3); + + const members = (response.body.members as ITeamMemberInfo[]).map(({ user, roles }) => ({ + _id: user._id, + username: user.username, + name: user.name, + roles, + })); + + expect(members).to.deep.own.include({ + _id: testUser._id, + username: testUser.username, + name: testUser.name, + roles: ['member', 'owner'], + }); + }), ) .then(() => done()) .catch(done); @@ -632,10 +662,10 @@ describe('[Teams]', () => { }); describe('/teams.removeMember', () => { - let testTeam; + let testTeam: ITeam; const teamName = `test-team-remove-member-${Date.now()}`; - let testUser; - let testUser2; + let testUser: TestUser; + let testUser2: TestUser; before(async () => { testUser = await createUser(); @@ -643,14 +673,14 @@ describe('[Teams]', () => { }); before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam = res.body.team; done(); }); @@ -659,7 +689,7 @@ describe('[Teams]', () => { after(() => Promise.all([deleteUser(testUser), deleteUser(testUser2), deleteTeam(credentials, teamName)])); it('should not be able to remove the last owner', (done) => { - request + void request .post(api('teams.removeMember')) .set(credentials) .send({ @@ -678,7 +708,7 @@ describe('[Teams]', () => { }); it('should not be able to remove if rooms is empty', (done) => { - request + void request .post(api('teams.removeMember')) .set(credentials) .send({ @@ -698,7 +728,7 @@ describe('[Teams]', () => { }); it('should remove one member from a public team', (done) => { - request + void request .post(api('teams.addMembers')) .set(credentials) .send({ @@ -749,10 +779,10 @@ describe('[Teams]', () => { }); describe('/teams.leave', () => { - let testTeam; + let testTeam: ITeam; const teamName = `test-team-leave-${Date.now()}`; - let testUser; - let testUser2; + let testUser: TestUser; + let testUser2: TestUser; before(async () => { testUser = await createUser(); @@ -760,14 +790,14 @@ describe('[Teams]', () => { }); before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam = res.body.team; done(); }); @@ -850,10 +880,10 @@ describe('[Teams]', () => { describe('/teams.info', () => { const teamName = `test-team-info-${Date.now()}`; - let testTeam; - let testTeam2; - let testUser; - let testUserCredentials; + let testTeam: ITeam; + let testTeam2: ITeam; + let testUser: TestUser; + let testUserCredentials: Credentials; before(async () => { testUser = await createUser(); @@ -936,11 +966,11 @@ describe('[Teams]', () => { describe('/teams.delete', () => { describe('deleting an empty team', () => { - let roomId; + let roomId: IRoom['_id']; const tempTeamName = `temporaryTeam-${Date.now()}`; before('create team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -976,7 +1006,7 @@ describe('[Teams]', () => { expect(res.body).to.have.property('success', true); }) .then(() => { - request + void request .get(api('teams.info')) .set(credentials) .query({ @@ -990,7 +1020,7 @@ describe('[Teams]', () => { expect(response.body.error).to.be.equal('Team not found'); }) .then(() => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -1014,12 +1044,12 @@ describe('[Teams]', () => { const tempTeamName = `temporaryTeam-${Date.now()}`; const channel1Name = `${tempTeamName}-channel1`; const channel2Name = `${tempTeamName}-channel2`; - let teamId; - let channel1Id; - let channel2Id; + let teamId: ITeam['_id']; + let channel1Id: IRoom['_id']; + let channel2Id: IRoom['_id']; before('create team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ @@ -1033,7 +1063,7 @@ describe('[Teams]', () => { }); before('create channel 1', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -1073,7 +1103,7 @@ describe('[Teams]', () => { }); before('create channel 2', (done) => { - request + void request .post(api('channels.create')) .set(credentials) .send({ @@ -1093,7 +1123,7 @@ describe('[Teams]', () => { }); before('add channel 2 to team', (done) => { - request + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -1127,7 +1157,7 @@ describe('[Teams]', () => { expect(res.body).to.have.property('success', true); }) .then(() => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -1141,7 +1171,7 @@ describe('[Teams]', () => { expect(response.body.error).to.include('[error-room-not-found]'); }) .then(() => { - request + void request .get(api('channels.info')) .set(credentials) .query({ @@ -1164,15 +1194,15 @@ describe('[Teams]', () => { }); describe('/teams.addRooms', () => { - let privateRoom; - let privateRoom2; - let privateRoom3; - let publicRoom; - let publicRoom2; - let publicTeam; - let privateTeam; - let testUser; - let testUserCredentials; + let privateRoom: IRoom & { t: 'p' }; + let privateRoom2: IRoom & { t: 'p' }; + let privateRoom3: IRoom & { t: 'p' }; + let publicRoom: IRoom & { t: 'c' }; + let publicRoom2: IRoom & { t: 'c' }; + let publicTeam: ITeam; + let privateTeam: ITeam; + let testUser: TestUser; + let testUserCredentials: Credentials; before(async () => { testUser = await createUser(); @@ -1196,8 +1226,8 @@ describe('[Teams]', () => { }); it('should throw an error if no permission', (done) => { - updatePermission('add-team-channel', []).then(() => { - request + void updatePermission('add-team-channel', []).then(() => { + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -1216,8 +1246,8 @@ describe('[Teams]', () => { }); it('should add public and private rooms to team', (done) => { - updatePermission('add-team-channel', ['admin']).then(() => { - request + void updatePermission('add-team-channel', ['admin']).then(() => { + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -1235,7 +1265,7 @@ describe('[Teams]', () => { expect(res.body.rooms[1]).to.have.property('_id'); expect(res.body.rooms[1]).to.have.property('teamId', publicTeam._id); - const rids = res.body.rooms.map(({ _id }) => _id); + const rids = (res.body.rooms as IRoom[]).map(({ _id }) => _id); expect(rids).to.include(publicRoom._id); expect(rids).to.include(privateRoom._id); @@ -1245,8 +1275,8 @@ describe('[Teams]', () => { }); it('should add public room to private team', (done) => { - updatePermission('add-team-channel', ['admin']).then(() => { - request + void updatePermission('add-team-channel', ['admin']).then(() => { + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -1266,8 +1296,8 @@ describe('[Teams]', () => { }); it('should add private room to team', (done) => { - updatePermission('add-team-channel', ['admin']).then(() => { - request + void updatePermission('add-team-channel', ['admin']).then(() => { + void request .post(api('teams.addRooms')) .set(credentials) .send({ @@ -1287,9 +1317,9 @@ describe('[Teams]', () => { }); it('should fail if the user cannot access the channel', (done) => { - updatePermission('add-team-channel', ['admin', 'user']) + void updatePermission('add-team-channel', ['admin', 'user']) .then(() => { - request + void request .post(api('teams.addRooms')) .set(testUserCredentials) .send({ @@ -1309,7 +1339,7 @@ describe('[Teams]', () => { }); it('should fail if the user is not the owner of the channel', (done) => { - request + void request .post(methodCall('addUsersToRoom')) .set(credentials) .send({ @@ -1326,7 +1356,7 @@ describe('[Teams]', () => { expect(res.body).to.have.property('success', true); }) .then(() => { - request + void request .post(api('teams.addRooms')) .set(testUserCredentials) .send({ @@ -1347,13 +1377,13 @@ describe('[Teams]', () => { }); describe('/teams.listRooms', () => { - let testUser; - let testUserCredentials; - let privateTeam; - let publicTeam; - let privateRoom; - let publicRoom; - let publicRoom2; + let testUser: TestUser; + let testUserCredentials: Credentials; + let privateTeam: ITeam; + let publicTeam: ITeam; + let privateRoom: IRoom; + let publicRoom: IRoom; + let publicRoom2: IRoom; before(async () => { testUser = await createUser(); @@ -1403,8 +1433,8 @@ describe('[Teams]', () => { ); it('should throw an error if team is private and no permission', (done) => { - updatePermission('view-all-teams', []).then(() => { - request + void updatePermission('view-all-teams', []).then(() => { + void request .get(api('teams.listRooms')) .set(testUserCredentials) .query({ @@ -1422,8 +1452,8 @@ describe('[Teams]', () => { }); it('should return only public rooms for public team', (done) => { - updatePermission('view-all-team-channels', []).then(() => { - request + void updatePermission('view-all-team-channels', []).then(() => { + void request .get(api('teams.listRooms')) .set(testUserCredentials) .query({ @@ -1443,8 +1473,8 @@ describe('[Teams]', () => { }); it('should return all rooms for public team', (done) => { - updatePermission('view-all-team-channels', ['user']).then(() => { - request + void updatePermission('view-all-team-channels', ['user']).then(() => { + void request .get(api('teams.listRooms')) .set(testUserCredentials) .query({ @@ -1462,8 +1492,8 @@ describe('[Teams]', () => { }); }); it('should return all rooms for public team even requested with count and offset params', (done) => { - updatePermission('view-all-team-channels', ['user']).then(() => { - request + void updatePermission('view-all-team-channels', ['user']).then(() => { + void request .get(api('teams.listRooms')) .set(testUserCredentials) .query({ @@ -1484,9 +1514,9 @@ describe('[Teams]', () => { }); it('should return public rooms for private team', (done) => { - updatePermission('view-all-team-channels', []).then(() => { - updatePermission('view-all-teams', ['admin']).then(() => { - request + void updatePermission('view-all-team-channels', []).then(() => { + void updatePermission('view-all-teams', ['admin']).then(() => { + void request .get(api('teams.listRooms')) .set(credentials) .query({ @@ -1505,9 +1535,9 @@ describe('[Teams]', () => { }); }); it('should return public rooms for private team even requested with count and offset params', (done) => { - updatePermission('view-all-team-channels', []).then(() => { - updatePermission('view-all-teams', ['admin']).then(() => { - request + void updatePermission('view-all-team-channels', []).then(() => { + void updatePermission('view-all-teams', ['admin']).then(() => { + void request .get(api('teams.listRooms')) .set(credentials) .query({ @@ -1530,8 +1560,8 @@ describe('[Teams]', () => { }); describe('/teams.updateRoom', () => { - let publicRoom; - let publicTeam; + let publicRoom: IRoom; + let publicTeam: ITeam; const name = `teamName-update-room-${Date.now()}`; before(async () => { @@ -1555,8 +1585,8 @@ describe('[Teams]', () => { }); it('should throw an error if no permission', (done) => { - updatePermission('edit-team-channel', []).then(() => { - request + void updatePermission('edit-team-channel', []).then(() => { + void request .post(api('teams.updateRoom')) .set(credentials) .send({ @@ -1575,8 +1605,8 @@ describe('[Teams]', () => { }); it('should set room to team default', (done) => { - updatePermission('edit-team-channel', ['admin']).then(() => { - request + void updatePermission('edit-team-channel', ['admin']).then(() => { + void request .post(api('teams.updateRoom')) .set(credentials) .send({ @@ -1596,10 +1626,10 @@ describe('[Teams]', () => { }); describe('team auto-join', () => { - let testTeam; - let createdRoom; - let testUser1; - let testUser2; + let testTeam: ITeam; + let createdRoom: IRoom; + let testUser1: TestUser; + let testUser2: TestUser; before(async () => { const [testUser1Result, testUser2Result] = await Promise.all([createUser(), createUser()]); @@ -1614,21 +1644,19 @@ describe('[Teams]', () => { const [testTeamCreationResult, testRoomCreationResult] = await Promise.all([createTeamPromise, createRoomPromise]); testTeam = testTeamCreationResult; - createdRoom = testRoomCreationResult; + createdRoom = testRoomCreationResult.body.channel; await request .post(api('teams.addRooms')) .set(credentials) .expect(200) .send({ - rooms: [createdRoom.body.channel._id], + rooms: [createdRoom._id], teamName: testTeam.name, }); }); - afterEach(() => - Promise.all([deleteTeam(credentials, testTeam.name), deleteRoom({ roomId: createdRoom.body.channel._id, type: 'c' })]), - ); + afterEach(() => Promise.all([deleteTeam(credentials, testTeam.name), deleteRoom({ roomId: createdRoom._id, type: 'c' })])); after(() => Promise.all([updateSetting('API_User_Limit', 500), deleteUser(testUser1), deleteUser(testUser2)])); @@ -1640,7 +1668,7 @@ describe('[Teams]', () => { .post(api('teams.updateRoom')) .set(credentials) .send({ - roomId: createdRoom.body.channel._id, + roomId: createdRoom._id, isDefault: true, }) .expect(200) @@ -1658,7 +1686,7 @@ describe('[Teams]', () => { .post(api('teams.updateRoom')) .set(credentials) .send({ - roomId: createdRoom.body.channel._id, + roomId: createdRoom._id, isDefault: true, }) .expect(200) @@ -1671,8 +1699,8 @@ describe('[Teams]', () => { }); describe('/teams.removeRoom', () => { - let publicRoom; - let publicTeam; + let publicRoom: IRoom; + let publicTeam: ITeam; const name = `teamName-remove-room-${Date.now()}`; before(async () => { @@ -1704,8 +1732,8 @@ describe('[Teams]', () => { ); it('should throw an error if no permission', (done) => { - updatePermission('remove-team-channel', []).then(() => { - request + void updatePermission('remove-team-channel', []).then(() => { + void request .post(api('teams.removeRoom')) .set(credentials) .send({ @@ -1724,8 +1752,8 @@ describe('[Teams]', () => { }); it('should remove room from team', (done) => { - updatePermission('remove-team-channel', ['admin']).then(() => { - request + void updatePermission('remove-team-channel', ['admin']).then(() => { + void request .post(api('teams.removeRoom')) .set(credentials) .send({ @@ -1746,53 +1774,53 @@ describe('[Teams]', () => { }); describe('/teams.update', () => { - let testTeam; - let testTeam2; - let testTeam3; + let testTeam: ITeam; + let testTeam2: ITeam; + let testTeam3: ITeam; const teamName = `test-team-name1${Date.now()}`; const teamName2 = `test-team-name2${Date.now()}`; const teamName3 = `test-team-name3${Date.now()}`; const testTeamName = `test-team-name-changed${Date.now()}-1`; const testTeamName2 = `test-team-name-changed${Date.now()}-2`; - let unauthorizedUser; + let unauthorizedUser: TestUser; before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam = res.body.team; done(); }); }); before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName2, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam2 = res.body.team; done(); }); }); before('Create test team', (done) => { - request + void request .post(api('teams.create')) .set(credentials) .send({ name: teamName3, type: 0, }) - .end((err, res) => { + .end((_err, res) => { testTeam3 = res.body.team; done(); }); @@ -1889,9 +1917,9 @@ describe('[Teams]', () => { }); describe('should update team room to default and invite users with the right notification preferences', () => { - let userWithPrefs; - let userCredentials; - let createdRoom; + let userWithPrefs: TestUser; + let userCredentials: Credentials; + let createdRoom: IRoom; before(async () => { userWithPrefs = await createUser(); @@ -1926,7 +1954,7 @@ describe('[Teams]', () => { }); it('should add user with prefs to team', (done) => { - request + void request .post(api('teams.addMembers')) .set(credentials) .send({ @@ -1950,7 +1978,7 @@ describe('[Teams]', () => { }); it('should return the user subscription with the right notification preferences', (done) => { - request + void request .get(api('subscriptions.getOne')) .set(userCredentials) .query({ diff --git a/apps/meteor/tests/end-to-end/api/26-LDAP.ts b/apps/meteor/tests/end-to-end/api/26-LDAP.ts index b4bb796b1e521..fc5c9d127836e 100644 --- a/apps/meteor/tests/end-to-end/api/26-LDAP.ts +++ b/apps/meteor/tests/end-to-end/api/26-LDAP.ts @@ -2,11 +2,9 @@ import { expect } from 'chai'; import { before, describe, it } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; - -describe('LDAP', function () { - this.retries(0); +import { getCredentials, api, request, credentials } from '../../data/api-data'; +describe('LDAP', () => { before((done) => getCredentials(done)); describe('[/ldap.syncNow]', () => { diff --git a/apps/meteor/tests/end-to-end/api/27-moderation.ts b/apps/meteor/tests/end-to-end/api/27-moderation.ts index 42845c4181a86..162b4c65c4bc5 100644 --- a/apps/meteor/tests/end-to-end/api/27-moderation.ts +++ b/apps/meteor/tests/end-to-end/api/27-moderation.ts @@ -4,14 +4,29 @@ import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../data/api-data'; -import { getUsersReports, reportUser } from '../../data/moderation.helper'; -import { createUser, deleteUser } from '../../data/users.helper.js'; +import { createUser, deleteUser } from '../../data/users.helper'; -// test for the /moderation.reportsByUsers endpoint +const makeModerationApiRequest = async ( + url: 'moderation.reportUser' | 'moderation.user.reportsByUserId', + method: 'get' | 'post', + data?: any, +) => { + let res: any; -describe('[Moderation]', function () { - this.retries(0); + if (method === 'get') { + res = await request.get(api(url)).set(credentials).query(data); + } else if (method === 'post') { + res = await request.post(api(url)).set(credentials).send(data); + } + return res.body; +}; + +const reportUser = (userId: string, reason: string) => makeModerationApiRequest('moderation.reportUser', 'post', { userId, reason }); + +const getUsersReports = (userId: string) => makeModerationApiRequest('moderation.user.reportsByUserId', 'get', { userId }); + +describe('[Moderation]', () => { before((done) => getCredentials(done)); describe('[/moderation.reportsByUsers]', () => { diff --git a/apps/meteor/tests/end-to-end/api/27-presence.ts b/apps/meteor/tests/end-to-end/api/27-presence.ts index 80a95e18e5b37..a622ddeddfe49 100644 --- a/apps/meteor/tests/end-to-end/api/27-presence.ts +++ b/apps/meteor/tests/end-to-end/api/27-presence.ts @@ -2,14 +2,13 @@ import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updatePermission } from '../../data/permissions.helper'; import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('[Presence]', function () { +describe('[Presence]', () => { let createdUser: any; - this.retries(0); before((done) => getCredentials(done)); diff --git a/apps/meteor/tests/end-to-end/api/28-roles.ts b/apps/meteor/tests/end-to-end/api/28-roles.ts index ad0e693bf2d85..91f7a30ef2f58 100644 --- a/apps/meteor/tests/end-to-end/api/28-roles.ts +++ b/apps/meteor/tests/end-to-end/api/28-roles.ts @@ -2,11 +2,9 @@ import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; - -describe('[Roles]', function () { - this.retries(0); +import { getCredentials, api, request, credentials } from '../../data/api-data'; +describe('[Roles]', () => { const isEnterprise = Boolean(process.env.IS_EE); before((done) => getCredentials(done)); diff --git a/apps/meteor/tests/end-to-end/api/29-oauth-server.ts b/apps/meteor/tests/end-to-end/api/29-oauth-server.ts index 16bed108b3418..fcae24a2068aa 100644 --- a/apps/meteor/tests/end-to-end/api/29-oauth-server.ts +++ b/apps/meteor/tests/end-to-end/api/29-oauth-server.ts @@ -2,11 +2,9 @@ import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; - -describe('[OAuth Server]', function () { - this.retries(0); +import { getCredentials, api, request, credentials } from '../../data/api-data'; +describe('[OAuth Server]', () => { let oAuthAppId: string; let clientId: string; let clientSecret: string; diff --git a/apps/meteor/tests/end-to-end/api/30-calendar.ts b/apps/meteor/tests/end-to-end/api/30-calendar.ts index 6ea4947fa9dbf..3ce9cb4159cb5 100644 --- a/apps/meteor/tests/end-to-end/api/30-calendar.ts +++ b/apps/meteor/tests/end-to-end/api/30-calendar.ts @@ -1,16 +1,16 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { password } from '../../data/user'; import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('[Calendar Events]', function () { - this.retries(0); - - let user2: Awaited> | undefined; - let userCredentials: Awaited> | undefined; +describe('[Calendar Events]', () => { + let user2: IUser; + let userCredentials: Credentials; before((done) => getCredentials(done)); diff --git a/apps/meteor/tests/end-to-end/api/31-failed-login-attempts.ts b/apps/meteor/tests/end-to-end/api/31-failed-login-attempts.ts index 7e1019b60ecb4..ee0236c591b6a 100644 --- a/apps/meteor/tests/end-to-end/api/31-failed-login-attempts.ts +++ b/apps/meteor/tests/end-to-end/api/31-failed-login-attempts.ts @@ -1,15 +1,15 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, beforeEach, afterEach, describe, it } from 'mocha'; import { sleep } from '../../../lib/utils/sleep'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updateSetting, updatePermission } from '../../data/permissions.helper'; import { password } from '../../data/user'; +import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser } from '../../data/users.helper'; -describe('[Failed Login Attempts]', function () { - this.retries(0); - +describe('[Failed Login Attempts]', () => { const maxAttemptsByUser = 2; const maxAttemptsByIp = 4; const userBlockSeconds = 3; @@ -103,7 +103,7 @@ describe('[Failed Login Attempts]', function () { } describe('[Block by User]', () => { - let user: Awaited> | undefined; + let user: TestUser; before(async () => { await updateSetting('Block_Multiple_Failed_Logins_By_Ip', false); @@ -159,9 +159,9 @@ describe('[Failed Login Attempts]', function () { }); describe('[Block by IP]', () => { - let user: Awaited> | undefined; - let user2: Awaited> | undefined; - let userLogin: Awaited> | undefined; + let user: TestUser; + let user2: TestUser; + let userLogin: TestUser; beforeEach(async () => { user = await createUser(); diff --git a/apps/meteor/tests/end-to-end/api/32-assets.ts b/apps/meteor/tests/end-to-end/api/32-assets.ts index 76c24a99765b5..63de4dc955fa6 100644 --- a/apps/meteor/tests/end-to-end/api/32-assets.ts +++ b/apps/meteor/tests/end-to-end/api/32-assets.ts @@ -1,10 +1,8 @@ import { describe, it } from 'mocha'; -import { request } from '../../data/api-data.js'; - -describe('assets', function () { - this.retries(0); +import { request } from '../../data/api-data'; +describe('assets', () => { it('should always have CORS headers for assets', async () => { await request.get('/assets/favicon.svg').expect('Content-Type', 'image/svg+xml').expect('Access-Control-Allow-Origin', '*').expect(200); diff --git a/apps/meteor/tests/end-to-end/api/33-federation.ts b/apps/meteor/tests/end-to-end/api/33-federation.ts index b44711d83291c..9d832d9fc1acf 100644 --- a/apps/meteor/tests/end-to-end/api/33-federation.ts +++ b/apps/meteor/tests/end-to-end/api/33-federation.ts @@ -4,9 +4,7 @@ import { after, before, describe, it } from 'mocha'; import { getCredentials, request } from '../../data/api-data'; import { updateSetting } from '../../data/permissions.helper'; -describe('federation', function () { - this.retries(0); - +describe('federation', () => { before((done) => getCredentials(done)); describe('well-known', () => { diff --git a/apps/meteor/tests/end-to-end/api/import.spec.ts b/apps/meteor/tests/end-to-end/api/import.spec.ts index ba37465626d39..a3db33efff0d5 100644 --- a/apps/meteor/tests/end-to-end/api/import.spec.ts +++ b/apps/meteor/tests/end-to-end/api/import.spec.ts @@ -2,13 +2,11 @@ import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../data/api-data.js'; +import { getCredentials, api, request, credentials } from '../../data/api-data'; import { password } from '../../data/user'; -import { createUser, login, deleteUser } from '../../data/users.helper.js'; - -describe('Imports', function () { - this.retries(0); +import { createUser, login, deleteUser } from '../../data/users.helper'; +describe('Imports', () => { before((done) => getCredentials(done)); describe('[/getCurrentImportOperation]', () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index b40ff1dce9c85..5c881b530d084 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -2,17 +2,18 @@ import fs from 'fs'; import path from 'path'; import { faker } from '@faker-js/faker'; +import type { Credentials } from '@rocket.chat/api-client'; import type { IOmnichannelRoom, ILivechatVisitor, - IUser, IOmnichannelSystemMessage, ILivechatPriority, ILivechatDepartment, + ISubscription, } from '@rocket.chat/core-typings'; import { LivechatPriorityWeight } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; import type { SuccessResult } from '../../../../app/api/server/definition'; @@ -46,13 +47,24 @@ import { updatePermission, updateSetting, } from '../../../data/permissions.helper'; -import { getSubscriptionForRoom } from '../../../data/subscriptions'; import { adminUsername, password } from '../../../data/user'; -import { createUser, deleteUser, login } from '../../../data/users.helper.js'; +import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -describe('LIVECHAT - rooms', function () { - this.retries(0); +const getSubscriptionForRoom = async (roomId: string, overrideCredential?: Credentials): Promise => { + const response = await request + .get(api('subscriptions.getOne')) + .set(overrideCredential || credentials) + .query({ roomId }) + .expect('Content-Type', 'application/json') + .expect(200); + + const { subscription } = response.body; + + return subscription; +}; + +describe('LIVECHAT - rooms', () => { let visitor: ILivechatVisitor; let room: IOmnichannelRoom; @@ -145,7 +157,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "agents" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?agents=invalid')) + .get(api('livechat/rooms')) + .query({ agents: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -155,7 +168,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "roomName" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?roomName[]=invalid')) + .get(api('livechat/rooms')) + .query({ 'roomName[]': 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -165,7 +179,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "departmentId" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?departmentId[]=marcos')) + .get(api('livechat/rooms')) + .query({ 'departmentId[]': 'marcos' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -175,7 +190,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "open" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?open[]=true')) + .get(api('livechat/rooms')) + .query({ 'open[]': 'true' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -185,7 +201,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "tags" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?tags=invalid')) + .get(api('livechat/rooms')) + .query({ tags: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -195,7 +212,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "createdAt" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?createdAt=invalid')) + .get(api('livechat/rooms')) + .query({ createdAt: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -205,7 +223,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "closedAt" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?closedAt=invalid')) + .get(api('livechat/rooms')) + .query({ closedAt: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -215,7 +234,8 @@ describe('LIVECHAT - rooms', function () { }); it('should return an error when the "customFields" query parameter is not valid', async () => { await request - .get(api('livechat/rooms?customFields=invalid')) + .get(api('livechat/rooms')) + .query({ customFields: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -372,10 +392,7 @@ describe('LIVECHAT - rooms', function () { agent: agent.credentials, }); - const { body } = await request - .get(api(`livechat/rooms?agents[]=${agent.user._id}`)) - .set(credentials) - .expect(200); + const { body } = await request.get(api('livechat/rooms')).query({ 'agents[]': agent.user._id }).set(credentials).expect(200); expect(body.rooms.length).to.be.equal(1); expect(body.rooms.some((room: IOmnichannelRoom) => room._id === expectedRoom._id)).to.be.true; @@ -386,10 +403,7 @@ describe('LIVECHAT - rooms', function () { const { room: expectedRoom } = await startANewLivechatRoomAndTakeIt(); await closeOmnichannelRoom(expectedRoom._id, [tag.name]); - const { body } = await request - .get(api(`livechat/rooms?tags[]=${tag.name}`)) - .set(credentials) - .expect(200); + const { body } = await request.get(api('livechat/rooms')).query({ 'tags[]': tag.name }).set(credentials).expect(200); expect(body.rooms.length).to.be.equal(1); expect(body.rooms.some((room: IOmnichannelRoom) => room._id === expectedRoom._id)).to.be.true; @@ -488,7 +502,7 @@ describe('LIVECHAT - rooms', function () { room: { _id: roomId }, } = await startANewLivechatRoomAndTakeIt(); - const manager: IUser = await createUser(); + const manager = await createUser(); const managerCredentials = await login(manager.username, password); await createManager(manager.username); @@ -659,7 +673,7 @@ describe('LIVECHAT - rooms', function () { }); it('should return a success message when transferred successfully to agent', async () => { - const initialAgentAssignedToChat: IUser = await createUser(); + const initialAgentAssignedToChat = await createUser(); const initialAgentCredentials = await login(initialAgentAssignedToChat.username, password); await createAgent(initialAgentAssignedToChat.username); await makeAgentAvailable(initialAgentCredentials); @@ -668,7 +682,7 @@ describe('LIVECHAT - rooms', function () { // at this point, the chat will get transferred to agent "user" const newRoom = await createLivechatRoom(newVisitor.token); - const forwardChatToUser: IUser = await createUser(); + const forwardChatToUser = await createUser(); const forwardChatToUserCredentials = await login(forwardChatToUser.username, password); await createAgent(forwardChatToUser.username); await makeAgentAvailable(forwardChatToUserCredentials); @@ -839,7 +853,7 @@ describe('LIVECHAT - rooms', function () { await makeAgentUnavailable(offlineAgent.credentials); - const manager: IUser = await createUser(); + const manager = await createUser(); const managerCredentials = await login(manager.username, password); await createManager(manager.username); @@ -1544,7 +1558,7 @@ describe('LIVECHAT - rooms', function () { }); it('should return the transfer history for a room', async () => { await updatePermission('view-l-room', ['admin', 'livechat-manager', 'livechat-agent']); - const initialAgentAssignedToChat: IUser = await createUser(); + const initialAgentAssignedToChat = await createUser(); const initialAgentCredentials = await login(initialAgentAssignedToChat.username, password); await createAgent(initialAgentAssignedToChat.username); await makeAgentAvailable(initialAgentCredentials); @@ -1553,7 +1567,7 @@ describe('LIVECHAT - rooms', function () { // at this point, the chat will get transferred to agent "user" const newRoom = await createLivechatRoom(newVisitor.token); - const forwardChatToUser: IUser = await createUser(); + const forwardChatToUser = await createUser(); const forwardChatToUserCredentials = await login(forwardChatToUser.username, password); await createAgent(forwardChatToUser.username); await makeAgentAvailable(forwardChatToUserCredentials); @@ -1980,7 +1994,7 @@ describe('LIVECHAT - rooms', function () { (IS_EE ? describe : describe.skip)('livechat/room/:rid/priority', async () => { let priorities: ILivechatPriority[]; let chosenPriority: ILivechatPriority; - this.afterAll(async () => { + after(async () => { await updateEEPermission('manage-livechat-priorities', ['admin', 'livechat-manager']); await updatePermission('view-l-room', ['admin', 'livechat-manager', 'livechat-agent']); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts index 141cfec1f73dc..fc81488ac37d2 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts @@ -1,3 +1,4 @@ +import type { Credentials } from '@rocket.chat/api-client'; import { UserStatus, type ILivechatAgent, type ILivechatDepartment, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -18,16 +19,14 @@ import { closeOmnichannelRoom, } from '../../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -import type { IUserCredentialsHeader } from '../../../data/user'; import { password } from '../../../data/user'; import { createUser, deleteUser, getMe, login, setUserStatus } from '../../../data/users.helper'; -describe('LIVECHAT - Agents', function () { - this.retries(0); +describe('LIVECHAT - Agents', () => { let agent: ILivechatAgent; let manager: ILivechatAgent; - let agent2: { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; + let agent2: { user: IUser; credentials: Credentials }; before((done) => getCredentials(done)); @@ -39,7 +38,7 @@ describe('LIVECHAT - Agents', function () { }); before(async () => { - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); await createAgent(user.username); @@ -243,7 +242,7 @@ describe('LIVECHAT - Agents', function () { it('should return a valid user when all goes fine', async () => { await updatePermission('view-livechat-manager', ['admin']); - const user: IUser = await createUser(); + const user = await createUser(); await request .post(api('livechat/users/agent')) .set(credentials) @@ -264,7 +263,7 @@ describe('LIVECHAT - Agents', function () { }); it('should properly create a manager', async () => { - const user: IUser = await createUser(); + const user = await createUser(); await request .post(api('livechat/users/manager')) .set(credentials) @@ -331,7 +330,7 @@ describe('LIVECHAT - Agents', function () { it('should return { user: null } when user is not an agent', async () => { await updatePermission('view-livechat-manager', ['admin']); - const user: IUser = await createUser(); + const user = await createUser(); await request .get(api(`livechat/users/agent/${user._id}`)) .set(credentials) @@ -482,7 +481,7 @@ describe('LIVECHAT - Agents', function () { await updatePermission('manage-livechat-agents', ['admin']); }); it('should return an error if user is not an agent', async () => { - const user: IUser = await createUser({ roles: ['livechat-manager'] }); + const user = await createUser({ roles: ['livechat-manager'] }); const userCredentials = await login(user.username, password); await request .post(api('livechat/agent.status')) @@ -520,7 +519,7 @@ describe('LIVECHAT - Agents', function () { }); }); it('should change logged in users status', async () => { - const currentUser: ILivechatAgent = await getMe(agent2.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent2.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; @@ -537,7 +536,7 @@ describe('LIVECHAT - Agents', function () { it('should allow managers to change other agents status', async () => { await updatePermission('manage-livechat-agents', ['admin']); - const currentUser: ILivechatAgent = await getMe(agent2.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent2.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; @@ -554,7 +553,7 @@ describe('LIVECHAT - Agents', function () { it('should throw an error if agent tries to make themselves available outside of Business hour', async () => { await makeDefaultBusinessHourActiveAndClosed(); - const currentUser: ILivechatAgent = await getMe(agent2.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent2.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; @@ -571,7 +570,7 @@ describe('LIVECHAT - Agents', function () { it('should not allow managers to make other agents available outside business hour', async () => { await updatePermission('manage-livechat-agents', ['admin']); - const currentUser: ILivechatAgent = await getMe(agent2.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent2.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; @@ -590,7 +589,7 @@ describe('LIVECHAT - Agents', function () { }); describe('Agent sidebar', () => { - let testUser: { user: IUser; credentials: IUserCredentialsHeader }; + let testUser: { user: IUser; credentials: Credentials }; before(async () => { const user = await createUser(); await createAgent(user.username); @@ -603,7 +602,7 @@ describe('LIVECHAT - Agents', function () { }; }); after(async () => { - await deleteUser(testUser.user._id); + await deleteUser(testUser.user); }); it('should return an empty list of rooms for a newly created agent', async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts b/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts index de2e81b370aab..030cdbd68f2df 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts @@ -7,9 +7,7 @@ import { sleep } from '../../../data/livechat/utils'; import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; import { IS_EE } from '../../../e2e/config/constants'; -describe('LIVECHAT - appearance', function () { - this.retries(0); - +describe('LIVECHAT - appearance', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts index 5645392b1adc4..75c1eaa622906 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts @@ -7,9 +7,7 @@ import { createCustomField } from '../../../data/livechat/custom-fields'; import { createVisitor } from '../../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -describe('LIVECHAT - custom fields', function () { - this.retries(0); - +describe('LIVECHAT - custom fields', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts index bfe53d92dade7..c0a559bbcba75 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; @@ -18,11 +19,9 @@ import { import { createAnOnlineAgent } from '../../../data/livechat/users'; import { sleep } from '../../../data/livechat/utils'; import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting } from '../../../data/permissions.helper'; -import type { IUserCredentialsHeader } from '../../../data/user'; import { IS_EE } from '../../../e2e/config/constants'; describe('LIVECHAT - dashboards', function () { - this.retries(0); // This test is expected to take more time since we're simulating real time conversations to verify analytics this.timeout(60000); @@ -34,7 +33,7 @@ describe('LIVECHAT - dashboards', function () { let department: ILivechatDepartment; const agents: { - credentials: IUserCredentialsHeader; + credentials: Credentials; user: IUser & { username: string }; }[] = []; let avgClosedRoomChatDuration = 0; @@ -143,7 +142,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/conversation-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/conversation-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -151,7 +154,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an array of conversation totalizers', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/conversation-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/conversation-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -207,7 +214,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/productivity-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -215,7 +226,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an array of productivity totalizers', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/productivity-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -263,7 +278,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/chats-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/chats-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -271,7 +290,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an array of chats totalizers', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/chats-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/chats-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -323,9 +346,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get( - api('livechat/analytics/dashboards/agents-productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'), - ) + .get(api('livechat/analytics/dashboards/agents-productivity-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -333,9 +358,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an array of agents productivity totalizers', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get( - api('livechat/analytics/dashboards/agents-productivity-totalizers?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z'), - ) + .get(api('livechat/analytics/dashboards/agents-productivity-totalizers')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -382,7 +409,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/chats?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/chats')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -390,7 +421,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an array of productivity totalizers', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/chats?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/chats')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -431,7 +466,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/chats-per-agent?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/chats-per-agent')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -439,7 +478,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an object with open and closed chats by agent', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/chats-per-agent?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/chats-per-agent')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -532,7 +575,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/chats-per-department?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/chats-per-department')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -540,7 +587,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an object with open and closed chats by department', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/chats-per-department?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/chats-per-department')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -577,7 +628,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await removePermissionFromAllRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/timings?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/timings')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -585,7 +640,11 @@ describe('LIVECHAT - dashboards', function () { it('should return an object with open and closed chats by department', async () => { await restorePermissionToRoles('view-livechat-manager'); await request - .get(api('livechat/analytics/dashboards/charts/timings?start=2019-10-25T15:08:17.248Z&end=2019-12-08T15:08:17.248Z')) + .get(api('livechat/analytics/dashboards/charts/timings')) + .query({ + start: '2019-10-25T15:08:17.248Z', + end: '2019-12-08T15:08:17.248Z', + }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) diff --git a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts index 06b54a4bf10cf..8867d9c1b075c 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts @@ -1,3 +1,4 @@ +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatInquiryRecord, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; @@ -17,14 +18,11 @@ import { } from '../../../data/livechat/rooms'; import { parseMethodResponse } from '../../../data/livechat/utils'; import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; -import type { IUserCredentialsHeader } from '../../../data/user'; import { password } from '../../../data/user'; import { createUser, login, deleteUser } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -describe('LIVECHAT - inquiries', function () { - this.retries(0); - +describe('LIVECHAT - inquiries', () => { before((done) => getCredentials(done)); before(async () => { @@ -80,7 +78,8 @@ describe('LIVECHAT - inquiries', function () { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await updatePermission('view-l-room', []); await request - .get(api('livechat/inquiries.getOne?roomId=room-id')) + .get(api('livechat/inquiries.getOne')) + .query({ roomId: 'room-id' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); @@ -88,7 +87,8 @@ describe('LIVECHAT - inquiries', function () { it('should return a inquiry', async () => { await updatePermission('view-l-room', ['admin']); await request - .get(api('livechat/inquiries.getOne?roomId=room-id')) + .get(api('livechat/inquiries.getOne')) + .query({ roomId: 'room-id' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -105,7 +105,8 @@ describe('LIVECHAT - inquiries', function () { const room = await createLivechatRoom(visitor.token); const inquiry = await fetchInquiry(room._id); await request - .get(api(`livechat/inquiries.getOne?roomId=${room._id}`)) + .get(api(`livechat/inquiries.getOne`)) + .query({ roomId: room._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -231,7 +232,7 @@ describe('LIVECHAT - inquiries', function () { }); after(async () => { await updateSetting('Livechat_accept_chats_with_no_agents', false); - await deleteUser(testUser.user._id); + await deleteUser(testUser.user); }); it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { await updatePermission('view-l-room', []); @@ -316,7 +317,7 @@ describe('LIVECHAT - inquiries', function () { }); describe('livechat:returnAsInquiry', () => { - let testUser: { user: IUser; credentials: IUserCredentialsHeader }; + let testUser: { user: IUser; credentials: Credentials }; before(async () => { const user = await createUser(); await createAgent(user.username); @@ -329,7 +330,7 @@ describe('LIVECHAT - inquiries', function () { }; }); after(async () => { - await deleteUser(testUser.user._id); + await deleteUser(testUser.user); }); it('should throw an error if user doesnt have view-l-room permission', async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts b/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts index 5d7165ff74b01..bc4d6fa04dc41 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts @@ -6,9 +6,7 @@ import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -describe('LIVECHAT - Integrations', function () { - this.retries(0); - +describe('LIVECHAT - Integrations', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts b/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts index b0fc5e85b27e3..1d5ef110d3086 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts @@ -7,9 +7,7 @@ import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -describe('LIVECHAT - Queue', function () { - this.retries(0); - +describe('LIVECHAT - Queue', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts b/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts index 92409c2c89d1e..6168ed410f24f 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts @@ -7,9 +7,7 @@ import { createTrigger, fetchTriggers } from '../../../data/livechat/triggers'; import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; import { IS_EE } from '../../../e2e/config/constants'; -describe('LIVECHAT - triggers', function () { - this.retries(0); - +describe('LIVECHAT - triggers', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index 58484e857c405..5bc961087efcd 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -6,7 +6,6 @@ import moment from 'moment'; import { type Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { getLicenseInfo } from '../../../data/licenses.helper'; import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; import { makeAgentAvailable, @@ -22,8 +21,11 @@ import { updatePermission, updateSetting, removePermissionFromAllRoles, restoreP import { adminUsername } from '../../../data/user'; import { IS_EE } from '../../../e2e/config/constants'; -describe('LIVECHAT - visitors', function () { - this.retries(0); +const getLicenseInfo = (loadValues = false) => { + return request.get(api('licenses.info')).set(credentials).query({ loadValues }).expect(200); +}; + +describe('LIVECHAT - visitors', () => { let visitor: ILivechatVisitor; before((done) => getCredentials(done)); @@ -221,7 +223,8 @@ describe('LIVECHAT - visitors', function () { await updatePermission('view-l-room', []); await request - .get(api('livechat/visitors.info?visitorId=invalid')) + .get(api('livechat/visitors.info')) + .query({ visitorId: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) @@ -234,7 +237,8 @@ describe('LIVECHAT - visitors', function () { await updatePermission('view-l-room', ['admin']); await request - .get(api('livechat/visitors.info?visitorId=invalid')) + .get(api('livechat/visitors.info')) + .query({ visitorId: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -245,7 +249,8 @@ describe('LIVECHAT - visitors', function () { }); it('should return the visitor info', async () => { await request - .get(api(`livechat/visitors.info?visitorId=${visitor._id}`)) + .get(api('livechat/visitors.info')) + .query({ visitorId: visitor._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -803,18 +808,25 @@ describe('LIVECHAT - visitors', function () { await request.get(api('omnichannel/contact.search')).set(credentials).expect('Content-Type', 'application/json').expect(400); }); it('should fail if its trying to find by an empty string', async () => { - await request.get(api('omnichannel/contact.search?email=')).set(credentials).expect('Content-Type', 'application/json').expect(400); + await request + .get(api('omnichannel/contact.search')) + .query({ email: '' }) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); }); it('should fail if custom is passed but is not JSON serializable', async () => { await request - .get(api('omnichannel/contact.search?custom={a":1}')) + .get(api('omnichannel/contact.search')) + .query({ custom: '{a":1}' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); }); it('should fail if custom is an empty object and no email|phone are provided', async () => { await request - .get(api('omnichannel/contact.search?custom={}')) + .get(api('omnichannel/contact.search')) + .query({ custom: '{}' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); @@ -822,7 +834,8 @@ describe('LIVECHAT - visitors', function () { it('should find a contact by email', async () => { const visitor = await createVisitor(); await request - .get(api(`omnichannel/contact.search?email=${visitor.visitorEmails?.[0].address}`)) + .get(api('omnichannel/contact.search')) + .query({ email: visitor.visitorEmails?.[0].address }) .set(credentials) .send() .expect('Content-Type', 'application/json') @@ -843,7 +856,8 @@ describe('LIVECHAT - visitors', function () { it('should find a contact by phone', async () => { const visitor = await createVisitor(); await request - .get(api(`omnichannel/contact.search?phone=${visitor.phone?.[0].phoneNumber}`)) + .get(api('omnichannel/contact.search')) + .query({ phone: visitor.phone?.[0].phoneNumber }) .set(credentials) .send() .expect('Content-Type', 'application/json') @@ -879,7 +893,8 @@ describe('LIVECHAT - visitors', function () { await createVisitor(); await request - .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ address: 'Rocket.Chat' })}`)) + .get(api('omnichannel/contact.search')) + .query({ custom: JSON.stringify({ address: 'Rocket.Chat' }) }) .set(credentials) .send() .expect('Content-Type', 'application/json') @@ -898,7 +913,8 @@ describe('LIVECHAT - visitors', function () { it('should return null if an invalid set of custom fields is passed and no other params are sent', async () => { const res = await request - .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ nope: 'nel' })}`)) + .get(api('omnichannel/contact.search')) + .query({ custom: JSON.stringify({ nope: 'nel' }) }) .set(credentials) .send(); expect(res.body).to.have.property('success', true); @@ -907,7 +923,8 @@ describe('LIVECHAT - visitors', function () { it('should not break if more than 1 custom field are passed', async () => { const res = await request - .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ nope: 'nel', another: 'field' })}`)) + .get(api('omnichannel/contact.search')) + .query({ custom: JSON.stringify({ nope: 'nel', another: 'field' }) }) .set(credentials) .send(); expect(res.body).to.have.property('success', true); @@ -916,7 +933,8 @@ describe('LIVECHAT - visitors', function () { it('should not break if bad things are passed as custom field keys', async () => { const res = await request - .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ $regex: 'nel' })}`)) + .get(api('omnichannel/contact.search')) + .query({ custom: JSON.stringify({ $regex: 'nel' }) }) .set(credentials) .send(); expect(res.body).to.have.property('success', true); @@ -925,7 +943,8 @@ describe('LIVECHAT - visitors', function () { it('should not break if bad things are passed as custom field keys 2', async () => { const res = await request - .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ '$regex: { very-bad }': 'nel' })}`)) + .get(api('omnichannel/contact.search')) + .query({ custom: JSON.stringify({ '$regex: { very-bad }': 'nel' }) }) .set(credentials) .send(); expect(res.body).to.have.property('success', true); @@ -934,7 +953,8 @@ describe('LIVECHAT - visitors', function () { it('should not break if bad things are passed as custom field values', async () => { const res = await request - .get(api(`omnichannel/contact.search?custom=${JSON.stringify({ nope: '^((ab)*)+$' })}`)) + .get(api('omnichannel/contact.search')) + .query({ custom: JSON.stringify({ nope: '^((ab)*)+$' }) }) .set(credentials) .send(); expect(res.body).to.have.property('success', true); @@ -1004,7 +1024,7 @@ describe('LIVECHAT - visitors', function () { describe('livechat/visitors.search', () => { it('should fail if user doesnt have view-l-room permission', async () => { await updatePermission('view-l-room', []); - const res = await request.get(api(`livechat/visitors.search?text=nel`)).set(credentials).send(); + const res = await request.get(api('livechat/visitors.search')).query({ text: 'nel' }).set(credentials).send(); expect(res.body).to.have.property('success', false); }); it('should fail if term is not on query params', async () => { @@ -1013,16 +1033,13 @@ describe('LIVECHAT - visitors', function () { expect(res.body).to.have.property('success', false); }); it('should not fail when term is an evil regex string', async () => { - const res = await request.get(api(`livechat/visitors.search?term=^((ab)*)+$`)).set(credentials).send(); + const res = await request.get(api('livechat/visitors.search')).query({ term: '^((ab)*)+$' }).set(credentials).send(); expect(res.body).to.have.property('success', true); }); it('should return a list of visitors when term is a valid string', async () => { const visitor = await createVisitor(); - const res = await request - .get(api(`livechat/visitors.search?term=${visitor.name}`)) - .set(credentials) - .send(); + const res = await request.get(api('livechat/visitors.search')).query({ term: visitor.name }).set(credentials).send(); expect(res.body).to.have.property('success', true); expect(res.body.visitors).to.be.an('array'); expect(res.body.visitors).to.have.lengthOf.greaterThan(0); @@ -1033,7 +1050,7 @@ describe('LIVECHAT - visitors', function () { expect(res.body.visitors[0]).to.have.property('phone'); }); it('should return a list of visitors when term is an empty string', async () => { - const res = await request.get(api(`livechat/visitors.search?term=`)).set(credentials).send(); + const res = await request.get(api('livechat/visitors.search')).query({ term: '' }).set(credentials).send(); expect(res.body).to.have.property('success', true); expect(res.body.visitors).to.be.an('array'); expect(res.body.visitors).to.have.lengthOf.greaterThan(0); @@ -1048,7 +1065,7 @@ describe('LIVECHAT - visitors', function () { let contact: ILivechatVisitor; it('should fail if user doesnt have view-l-room permission', async () => { await removePermissionFromAllRoles('view-l-room'); - const res = await request.get(api(`omnichannel/contact?text=nel`)).set(credentials).send(); + const res = await request.get(api('omnichannel/contact')).query({ text: 'nel' }).set(credentials).send(); expect(res.body).to.have.property('success', false); await restorePermissionToRoles('view-l-room'); diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts b/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts index 965e9b5ef58f5..53367f2655e73 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts @@ -209,7 +209,7 @@ describe('Email inbox', () => { it('should return an email inbox matching email', async () => { await createEmailInbox(); await updatePermission('manage-email-inbox', ['admin']); - await request.get(api(`email-inbox.search?email=test`)).set(credentials).expect(200); + await request.get(api('email-inbox.search')).query({ email: 'test' }).set(credentials).expect(200); }); }); }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts index 09d90b7da8d13..c07f7bcecc816 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts @@ -17,9 +17,7 @@ import { createBotAgent, getRandomVisitorToken } from '../../../data/livechat/us import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; import { IS_EE } from '../../../e2e/config/constants'; -describe('LIVECHAT - Utils', function () { - this.retries(0); - +describe('LIVECHAT - Utils', () => { before((done) => getCredentials(done)); after(async () => { @@ -116,7 +114,7 @@ describe('LIVECHAT - Utils', function () { (IS_EE ? it : it.skip)('should return online as true if there is at least one agent online', async () => { const { department } = await createDepartmentWithAnOnlineAgent(); - const { body } = await request.get(api(`livechat/config?department=${department._id}`)).set(credentials); + const { body } = await request.get(api('livechat/config')).query({ department: department._id }).set(credentials); expect(body).to.have.property('config'); expect(body.config).to.have.property('online', true); }); @@ -124,7 +122,7 @@ describe('LIVECHAT - Utils', function () { const { department, agent } = await createDepartmentWithAnOnlineAgent(); await makeAgentUnavailable(agent.credentials); - const { body } = await request.get(api(`livechat/config?department=${department._id}`)).set(credentials); + const { body } = await request.get(api('livechat/config')).query({ department: department._id }).set(credentials); expect(body).to.have.property('config'); expect(body.config).to.have.property('online', false); }); @@ -137,7 +135,7 @@ describe('LIVECHAT - Utils', function () { const botUser = await createBotAgent(); await addOrRemoveAgentFromDepartment(department._id, { agentId: botUser.user._id, username: botUser.user.username as string }, true); - const { body } = await request.get(api(`livechat/config?department=${department._id}`)).set(credentials); + const { body } = await request.get(api('livechat/config')).query({ department: department._id }).set(credentials); expect(body).to.have.property('config'); await updateSetting('Livechat_assign_new_conversation_to_bot', false); @@ -145,7 +143,7 @@ describe('LIVECHAT - Utils', function () { }); it('should return a guest if there exists a guest with the same token', async () => { const guest = await createVisitor(); - const { body } = await request.get(api(`livechat/config?token=${guest.token}`)).set(credentials); + const { body } = await request.get(api('livechat/config')).query({ token: guest.token }).set(credentials); expect(body).to.have.property('config'); expect(body.config).to.have.property('guest'); expect(body.config.guest).to.have.property('name', guest.name); @@ -153,13 +151,13 @@ describe('LIVECHAT - Utils', function () { it('should not return a guest if there exists a guest with the same token but the guest is not online', async () => { const token = getRandomVisitorToken(); - const { body } = await request.get(api(`livechat/config?token=${token}`)).set(credentials); + const { body } = await request.get(api('livechat/config')).query({ token }).set(credentials); expect(body).to.have.property('config'); expect(body.config).to.not.have.property('guest'); }); it('should return no online room if visitor is not chatting with an agent', async () => { const visitor = await createVisitor(); - const { body } = await request.get(api(`livechat/config?token=${visitor.token}`)).set(credentials); + const { body } = await request.get(api('livechat/config')).query({ token: visitor.token }).set(credentials); expect(body).to.have.property('config'); expect(body.config).to.not.have.property('room'); }); @@ -167,7 +165,7 @@ describe('LIVECHAT - Utils', function () { const newVisitor = await createVisitor(); const newRoom = await createLivechatRoom(newVisitor.token); - const { body } = await request.get(api(`livechat/config?token=${newVisitor.token}`)).set(credentials); + const { body } = await request.get(api('livechat/config')).query({ token: newVisitor.token }).set(credentials); expect(body).to.have.property('config'); expect(body.config).to.have.property('room'); @@ -470,7 +468,8 @@ describe('LIVECHAT - Utils', function () { const room2 = await createLivechatRoom(visitor2.token); const { body: result1 } = await request - .get(api('livechat/visitors.search?term=VisitorIn&sort={"lastChat.ts":1}')) + .get(api('livechat/visitors.search')) + .query({ term: 'VisitorIn', sort: '{"lastChat.ts":1}' }) .set(credentials) .send(); @@ -479,7 +478,8 @@ describe('LIVECHAT - Utils', function () { expect(result1.visitors[0].name).to.be.eq('VisitorInPast'); const { body: result2 } = await request - .get(api('livechat/visitors.search?term=VisitorIn&sort={"lastChat.ts":-1}')) + .get(api('livechat/visitors.search')) + .query({ term: 'VisitorIn', sort: '{"lastChat.ts":-1}' }) .set(credentials) .send(); diff --git a/apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts b/apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts index 3fb89278a7c1a..7b80e25286291 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/12-priorites.ts @@ -7,7 +7,7 @@ import type { } from '@rocket.chat/core-typings'; import { OmnichannelSortingMechanismSettingType } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { after, before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department'; @@ -32,9 +32,7 @@ import { import { IS_EE } from '../../../e2e/config/constants'; import { generateRandomSLAData } from '../../../e2e/utils/omnichannel/sla'; -(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Priorities & SLAs', function () { - this.retries(0); - +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Priorities & SLAs', () => { before((done) => getCredentials(done)); before(async () => { @@ -42,7 +40,7 @@ import { generateRandomSLAData } from '../../../e2e/utils/omnichannel/sla'; await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); }); - this.afterAll(async () => { + after(async () => { await addPermissions({ 'manage-livechat-priorities': ['admin', 'livechat-manager'], 'manage-livechat-sla': ['admin', 'livechat-manager'], diff --git a/apps/meteor/tests/end-to-end/api/livechat/13-tags.ts b/apps/meteor/tests/end-to-end/api/livechat/13-tags.ts index ff286250c5bbf..7953e772c016f 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/13-tags.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/13-tags.ts @@ -12,9 +12,7 @@ import { password } from '../../../data/user'; import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -(IS_EE ? describe : describe.skip)('[EE] Livechat - Tags', function () { - this.retries(0); - +(IS_EE ? describe : describe.skip)('[EE] Livechat - Tags', () => { before((done) => getCredentials(done)); before(async () => { @@ -68,7 +66,7 @@ import { IS_EE } from '../../../e2e/config/constants'; }); after(async () => { - await deleteUser(monitor); + await deleteUser(monitor.user); }); it('should throw unauthorized error when the user does not have the necessary permission', async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts index 3a03d9e93e2cf..425c776fecdb9 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts @@ -9,9 +9,7 @@ import { updatePermission, updateSetting } from '../../../data/permissions.helpe import { createUser, deleteUser } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Units', function () { - this.retries(0); - +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Units', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts index d4c9c758e5a08..a71925531f4d5 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts @@ -13,9 +13,7 @@ import { password } from '../../../data/user'; import { createUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Canned responses', function () { - this.retries(0); - +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Canned responses', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts b/apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts index 5f8d7492480f9..4a3869aaed383 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/16-video-call.ts @@ -5,9 +5,7 @@ import { getCredentials, api, request, credentials } from '../../../data/api-dat import { createLivechatRoom, createVisitor, fetchMessages, sendMessage } from '../../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -describe('LIVECHAT - WebRTC video call', function () { - this.retries(0); - +describe('LIVECHAT - WebRTC video call', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/17-dashboards-ee.ts b/apps/meteor/tests/end-to-end/api/livechat/17-dashboards-ee.ts index 98c1bbe957a38..dc379eea92127 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/17-dashboards-ee.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/17-dashboards-ee.ts @@ -17,9 +17,7 @@ import { import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { IS_EE } from '../../../e2e/config/constants'; -(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - dashboards', function () { - this.retries(0); - +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - dashboards', () => { before((done) => getCredentials(done)); before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts b/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts index e3117c3088a70..43282ef9c79be 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/18-rooms-ee.ts @@ -1,3 +1,4 @@ +import type { Credentials } from '@rocket.chat/api-client'; import type { IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -22,12 +23,10 @@ import { password } from '../../../data/user'; import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - rooms', function () { - this.retries(0); - +(IS_EE ? describe : describe.skip)('[EE] LIVECHAT - rooms', () => { before((done) => getCredentials(done)); - let agent2: { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; + let agent2: { user: IUser; credentials: Credentials }; before(async () => { await updateSetting('Livechat_enabled', true); @@ -37,7 +36,7 @@ import { IS_EE } from '../../../e2e/config/constants'; }); before(async () => { - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); await createAgent(user.username); await updateSetting('Livechat_allow_manual_on_hold', true); diff --git a/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts b/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts index 869f36fd9695b..9a6543f06c724 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/19-business-hours.ts @@ -1,3 +1,4 @@ +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatAgent, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; import { LivechatBusinessHourBehaviors, LivechatBusinessHourTypes, ILivechatAgentStatus } from '@rocket.chat/core-typings'; import { expect } from 'chai'; @@ -26,14 +27,12 @@ import { import { createAgent, createManager, makeAgentAvailable } from '../../../data/livechat/rooms'; import { removeAgent } from '../../../data/livechat/users'; import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting, updateEESetting } from '../../../data/permissions.helper'; -import type { IUserCredentialsHeader } from '../../../data/user'; import { password } from '../../../data/user'; +import type { TestUser } from '../../../data/users.helper'; import { setUserActiveStatus, createUser, deleteUser, getMe, getUserByUsername, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -describe('LIVECHAT - business hours', function () { - this.retries(0); - +describe('LIVECHAT - business hours', () => { before((done) => getCredentials(done)); before(async () => { @@ -317,7 +316,7 @@ describe('LIVECHAT - business hours', function () { const { department, agent } = await createDepartmentWithAnOnlineAgent(); await createCustomBusinessHour([department._id], false); - const latestAgent: ILivechatAgent = await getMe(agent.credentials as any); + const latestAgent = await getMe(agent.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(0); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); @@ -434,7 +433,7 @@ describe('LIVECHAT - business hours', function () { // archive department await archiveDepartment(deptLinkedToCustomBH._id); - const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials as any); + const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); @@ -479,7 +478,7 @@ describe('LIVECHAT - business hours', function () { expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // verify if other agent still has BH within his cache - const otherAgent: ILivechatAgent = await getMe(agent.credentials as any); + const otherAgent: ILivechatAgent = await getMe(agent.credentials); expect(otherAgent).to.be.an('object'); expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1); expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); @@ -579,7 +578,7 @@ describe('LIVECHAT - business hours', function () { // disable department await disableDepartment(deptLinkedToCustomBH); - const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials as any); + const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); @@ -617,13 +616,13 @@ describe('LIVECHAT - business hours', function () { expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id); // verify if overlapping agent still has BH within his cache - const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials as any); + const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // verify if other agent still has BH within his cache - const otherAgent: ILivechatAgent = await getMe(agent.credentials as any); + const otherAgent: ILivechatAgent = await getMe(agent.credentials); expect(otherAgent).to.be.an('object'); expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1); expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); @@ -710,7 +709,7 @@ describe('LIVECHAT - business hours', function () { await deleteDepartment(deptLinkedToCustomBH._id); - const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials as any); + const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); @@ -742,13 +741,13 @@ describe('LIVECHAT - business hours', function () { expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id); // verify if overlapping agent still has BH within his cache - const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials as any); + const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // verify if other agent still has BH within his cache - const otherAgent: ILivechatAgent = await getMe(agent.credentials as any); + const otherAgent: ILivechatAgent = await getMe(agent.credentials); expect(otherAgent).to.be.an('object'); expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1); expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); @@ -765,7 +764,7 @@ describe('LIVECHAT - business hours', function () { describe('[CE][BH] On Agent created/removed', () => { let defaultBH: ILivechatBusinessHour; let agent: ILivechatAgent; - let agentCredentials: IUserCredentialsHeader; + let agentCredentials: Credentials; before(async () => { await updateSetting('Livechat_enable_business_hours', true); @@ -787,7 +786,7 @@ describe('LIVECHAT - business hours', function () { it('should create a new agent and verify if it is assigned to the default business hour which is open', async () => { agent = await createAgent(agent.username); - const latestAgent: ILivechatAgent = await getMe(agentCredentials as any); + const latestAgent: ILivechatAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id); @@ -811,7 +810,7 @@ describe('LIVECHAT - business hours', function () { it('should verify if agent is assigned to BH when it is opened', async () => { // first verify if agent is not assigned to any BH - let latestAgent: ILivechatAgent = await getMe(agentCredentials as any); + let latestAgent: ILivechatAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(0); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); @@ -820,7 +819,7 @@ describe('LIVECHAT - business hours', function () { await openOrCloseBusinessHour(defaultBH, true); // verify if agent is assigned to BH - latestAgent = await getMe(agentCredentials as any); + latestAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id); @@ -832,7 +831,7 @@ describe('LIVECHAT - business hours', function () { it('should verify if BH related props are cleared when an agent is deleted', async () => { await removeAgent(agent._id); - const latestAgent: ILivechatAgent = await getMe(agentCredentials as any); + const latestAgent: ILivechatAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.undefined; expect(latestAgent.statusLivechat).to.be.undefined; @@ -840,7 +839,7 @@ describe('LIVECHAT - business hours', function () { describe('Special Case - Agent created, BH already enabled', () => { let newAgent: ILivechatAgent; - let newAgentCredentials: IUserCredentialsHeader; + let newAgentCredentials: Credentials; before(async () => { newAgent = await createUser({ roles: ['user', 'livechat-agent'] }); newAgentCredentials = await login(newAgent.username, password); @@ -849,7 +848,7 @@ describe('LIVECHAT - business hours', function () { await deleteUser(newAgent); }); it('should verify a newly created agent to be assigned to the default business hour', async () => { - const latestAgent: ILivechatAgent = await getMe(newAgentCredentials as any); + const latestAgent: ILivechatAgent = await getMe(newAgentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id); @@ -857,13 +856,13 @@ describe('LIVECHAT - business hours', function () { }); after(async () => { - await deleteUser(agent._id); + await deleteUser(agent); }); }); describe('[CE][BH] On Agent deactivated/activated', () => { let defaultBH: ILivechatBusinessHour; - let agent: ILivechatAgent; + let agent: TestUser; before(async () => { await updateSetting('Livechat_enable_business_hours', true); @@ -887,7 +886,7 @@ describe('LIVECHAT - business hours', function () { await makeAgentAvailable(await login(agent.username, password)); await setUserActiveStatus(agent._id, false); - const latestAgent = await getUserByUsername(agent.username); + const latestAgent = await getUserByUsername(agent.username); expect(latestAgent).to.be.an('object'); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); @@ -898,7 +897,7 @@ describe('LIVECHAT - business hours', function () { await setUserActiveStatus(agent._id, true); - const latestAgent = await getUserByUsername(agent.username); + const latestAgent = await getUserByUsername(agent.username); expect(latestAgent).to.be.an('object'); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.AVAILABLE); @@ -909,7 +908,7 @@ describe('LIVECHAT - business hours', function () { await setUserActiveStatus(agent._id, false); await setUserActiveStatus(agent._id, true); - const latestAgent = await getUserByUsername(agent.username); + const latestAgent = await getUserByUsername(agent.username); expect(latestAgent).to.be.an('object'); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); diff --git a/apps/meteor/tests/end-to-end/api/livechat/21-reports.ts b/apps/meteor/tests/end-to-end/api/livechat/21-reports.ts index bc74905f92f63..2de552b7d8e19 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/21-reports.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/21-reports.ts @@ -1,3 +1,4 @@ +import type { Credentials } from '@rocket.chat/api-client'; import type { IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -14,11 +15,11 @@ import { IS_EE } from '../../../e2e/config/constants'; (IS_EE ? describe : describe.skip)('LIVECHAT - reports', () => { before((done) => getCredentials(done)); - let agent2: { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; - let agent3: { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; + let agent2: { user: IUser; credentials: Credentials }; + let agent3: { user: IUser; credentials: Credentials }; before(async () => { - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); if (!user.username) { throw new Error('user not created'); @@ -32,7 +33,7 @@ import { IS_EE } from '../../../e2e/config/constants'; }); before(async () => { - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); await createAgent(); if (!user.username) { diff --git a/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts b/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts index 31d5e04c41629..ad24fb0e19815 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, it, describe, after } from 'mocha'; @@ -19,7 +20,7 @@ import { password } from '../../../data/user'; import { createUser, deleteUser, login, setUserActiveStatus } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; -type TestUser = { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; +type TestUser = { user: IUser; credentials: Credentials }; (IS_EE ? describe : describe.skip)('Omnichannel - Monitors', () => { let manager: TestUser; @@ -35,7 +36,7 @@ type TestUser = { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id await makeAgentAvailable(); }); before(async () => { - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); if (!user.username) { throw new Error('user not created'); @@ -48,7 +49,7 @@ type TestUser = { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id }; }); before(async () => { - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); if (!user.username) { throw new Error('user not created'); diff --git a/apps/meteor/tests/end-to-end/api/livechat/23-mac.ts b/apps/meteor/tests/end-to-end/api/livechat/23-mac.ts index f8f23ca8fb30c..033122c467da4 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/23-mac.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/23-mac.ts @@ -78,7 +78,8 @@ describe('MAC', () => { it('visitor should be marked as active for period', async () => { const { body } = await request - .get(api(`livechat/visitors.info?visitorId=${visitor._id}`)) + .get(api('livechat/visitors.info')) + .query({ visitorId: visitor._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200); diff --git a/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts b/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts index 48d447b9923db..fb1301341069a 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/24-routing.ts @@ -1,3 +1,4 @@ +import type { Credentials } from '@rocket.chat/api-client'; import { UserStatus, type ILivechatDepartment, type IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -14,7 +15,6 @@ import { } from '../../../data/livechat/rooms'; import { sleep } from '../../../data/livechat/utils'; import { updateSetting } from '../../../data/permissions.helper'; -import type { IUserCredentialsHeader } from '../../../data/user'; import { password } from '../../../data/user'; import { createUser, deleteUser, login, setUserActiveStatus, setUserStatus } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; @@ -31,8 +31,8 @@ import { IS_EE } from '../../../e2e/config/constants'; await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); }); - let testUser: { user: IUser; credentials: IUserCredentialsHeader }; - let testUser2: { user: IUser; credentials: IUserCredentialsHeader }; + let testUser: { user: IUser; credentials: Credentials }; + let testUser2: { user: IUser; credentials: Credentials }; let testDepartment: ILivechatDepartment; before(async () => { @@ -96,7 +96,7 @@ import { IS_EE } from '../../../e2e/config/constants'; await makeAgentUnavailable(testUser.credentials); const visitor = await createVisitor(testDepartment._id); - const { body } = await request.get(api(`livechat/room?token=${visitor.token}`)).expect(400); + const { body } = await request.get(api('livechat/room')).query({ token: visitor.token }).expect(400); expect(body.error).to.be.equal('Sorry, no online agents [no-agent-online]'); }); it('should accept a conversation but not route to anyone when Livechat_accept_chats_with_no_agents is true', async () => { @@ -153,8 +153,8 @@ import { IS_EE } from '../../../e2e/config/constants'; await updateSetting('Livechat_Routing_Method', 'Load_Balancing'); }); - let testUser: { user: IUser; credentials: IUserCredentialsHeader }; - let testUser2: { user: IUser; credentials: IUserCredentialsHeader }; + let testUser: { user: IUser; credentials: Credentials }; + let testUser2: { user: IUser; credentials: Credentials }; let testDepartment: ILivechatDepartment; before(async () => { @@ -242,8 +242,8 @@ import { IS_EE } from '../../../e2e/config/constants'; await updateSetting('Livechat_Routing_Method', 'Load_Rotation'); }); - let testUser: { user: IUser; credentials: IUserCredentialsHeader }; - let testUser2: { user: IUser; credentials: IUserCredentialsHeader }; + let testUser: { user: IUser; credentials: Credentials }; + let testUser2: { user: IUser; credentials: Credentials }; let testDepartment: ILivechatDepartment; before(async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/methods/changeLivechatStatus.ts b/apps/meteor/tests/end-to-end/api/livechat/methods/changeLivechatStatus.ts index d299acb157644..9067965f4d18d 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/methods/changeLivechatStatus.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/methods/changeLivechatStatus.ts @@ -1,3 +1,4 @@ +import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatAgent, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -10,17 +11,15 @@ import { updatePermission, updateSetting } from '../../../../data/permissions.he import { password } from '../../../../data/user'; import { createUser, deleteUser, getMe, login } from '../../../../data/users.helper'; -describe('livechat:changeLivechatStatus', function () { - this.retries(0); - - let agent: { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; +describe('livechat:changeLivechatStatus', () => { + let agent: { user: IUser; credentials: Credentials }; before((done) => getCredentials(done)); before(async () => { await updateSetting('Livechat_enabled', true); - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); await createAgent(user.username); @@ -60,7 +59,7 @@ describe('livechat:changeLivechatStatus', function () { await updatePermission('manage-livechat-agents', ['admin']); }); it('should return an error if user is not an agent', async () => { - const user: IUser = await createUser(); + const user = await createUser(); const userCredentials = await login(user.username, password); await request .post(methodCall('livechat:changeLivechatStatus')) @@ -128,7 +127,7 @@ describe('livechat:changeLivechatStatus', function () { }); }); it('should change logged in users status', async () => { - const currentUser: ILivechatAgent = await getMe(agent.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; @@ -153,7 +152,7 @@ describe('livechat:changeLivechatStatus', function () { it('should allow managers to change other agents status', async () => { await updatePermission('manage-livechat-agents', ['admin']); - const currentUser: ILivechatAgent = await getMe(agent.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; @@ -178,7 +177,7 @@ describe('livechat:changeLivechatStatus', function () { it('should throw an error if agent tries to make themselves available outside of Business hour', async () => { await makeDefaultBusinessHourActiveAndClosed(); - const currentUser: ILivechatAgent = await getMe(agent.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; @@ -204,7 +203,7 @@ describe('livechat:changeLivechatStatus', function () { it('should allow managers to make other agents available outside business hour', async () => { await updatePermission('manage-livechat-agents', ['admin']); - const currentUser: ILivechatAgent = await getMe(agent.credentials as any); + const currentUser: ILivechatAgent = await getMe(agent.credentials); const currentStatus = currentUser.statusLivechat; const newStatus = currentStatus === 'available' ? 'not-available' : 'available'; diff --git a/apps/meteor/tests/end-to-end/apps/00-installation.js b/apps/meteor/tests/end-to-end/apps/00-installation.ts similarity index 82% rename from apps/meteor/tests/end-to-end/apps/00-installation.js rename to apps/meteor/tests/end-to-end/apps/00-installation.ts index f8633439a7027..a3cfd3c17ce09 100644 --- a/apps/meteor/tests/end-to-end/apps/00-installation.js +++ b/apps/meteor/tests/end-to-end/apps/00-installation.ts @@ -1,15 +1,15 @@ import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, request, credentials, api } from '../../data/api-data.js'; -import { APP_URL, apps, APP_USERNAME } from '../../data/apps/apps-data.js'; -import { cleanupApps } from '../../data/apps/helper.js'; +import { getCredentials, request, credentials, api } from '../../data/api-data'; +import { APP_URL, apps } from '../../data/apps/apps-data'; +import { cleanupApps } from '../../data/apps/helper'; import { updatePermission } from '../../data/permissions.helper'; -import { getUserByUsername } from '../../data/users.helper.js'; +import { getUserByUsername } from '../../data/users.helper'; -describe('Apps - Installation', function () { - this.retries(0); +const APP_USERNAME = 'appsrocketchattester.bot'; +describe('Apps - Installation', () => { before((done) => getCredentials(done)); before(async () => cleanupApps()); @@ -18,8 +18,8 @@ describe('Apps - Installation', function () { describe('[Installation]', () => { it('should throw an error when trying to install an app and the apps framework is enabled but the user does not have the permission', (done) => { - updatePermission('manage-apps', []).then(() => { - request + void updatePermission('manage-apps', []).then(() => { + void request .post(apps()) .set(credentials) .send({ @@ -35,8 +35,8 @@ describe('Apps - Installation', function () { }); }); it('should install the app successfully from a URL', (done) => { - updatePermission('manage-apps', ['admin']).then(() => { - request + void updatePermission('manage-apps', ['admin']).then(() => { + void request .post(apps()) .set(credentials) .send({ @@ -55,7 +55,7 @@ describe('Apps - Installation', function () { }); }); it('should have created the app user successfully', (done) => { - getUserByUsername(APP_USERNAME) + void getUserByUsername(APP_USERNAME) .then((user) => { expect(user.username).to.be.equal(APP_USERNAME); }) @@ -63,8 +63,9 @@ describe('Apps - Installation', function () { }); describe('Slash commands registration', () => { it('should have created the "test-simple" slash command successfully', (done) => { - request - .get(api('commands.get?command=test-simple')) + void request + .get(api('commands.get')) + .query({ command: 'test-simple' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -76,8 +77,9 @@ describe('Apps - Installation', function () { .end(done); }); it('should have created the "test-with-arguments" slash command successfully', (done) => { - request - .get(api('commands.get?command=test-with-arguments')) + void request + .get(api('commands.get')) + .query({ command: 'test-with-arguments' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200) @@ -91,7 +93,7 @@ describe('Apps - Installation', function () { }); describe('Video Conf Provider registration', () => { it('should have created two video conf provider successfully', (done) => { - request + void request .get(api('video-conference.providers')) .set(credentials) .expect('Content-Type', 'application/json') diff --git a/apps/meteor/tests/end-to-end/apps/01-send-messages.js b/apps/meteor/tests/end-to-end/apps/01-send-messages.ts similarity index 82% rename from apps/meteor/tests/end-to-end/apps/01-send-messages.js rename to apps/meteor/tests/end-to-end/apps/01-send-messages.ts index 3f729715915fa..9cfb9f2688d1c 100644 --- a/apps/meteor/tests/end-to-end/apps/01-send-messages.js +++ b/apps/meteor/tests/end-to-end/apps/01-send-messages.ts @@ -1,15 +1,15 @@ +import type { App, IMessage, IRoom } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, request, credentials } from '../../data/api-data.js'; -import { apps } from '../../data/apps/apps-data.js'; -import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; -import { getMessageById } from '../../data/chat.helper.js'; +import { getCredentials, request, credentials } from '../../data/api-data'; +import { apps } from '../../data/apps/apps-data'; +import { cleanupApps, installTestApp } from '../../data/apps/helper'; +import { getMessageById } from '../../data/chat.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; -describe('Apps - Send Messages As APP User', function () { - this.retries(0); - let app; +describe('Apps - Send Messages As APP User', () => { + let app: App; before((done) => getCredentials(done)); before(async () => { @@ -21,25 +21,25 @@ describe('Apps - Send Messages As APP User', function () { describe('[Send Message as app user]', () => { it('should return an error when the room is not found', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-app-user`)) .send({ roomId: 'invalid-room', }) .set(credentials) .expect(404) - .expect((err, res) => { + .expect((err: unknown, res: undefined) => { expect(err).to.have.a.property('error'); expect(res).to.be.equal(undefined); - expect(err.error).to.have.a.property('text'); - expect(err.error.text).to.be.equal('Room "invalid-room" could not be found'); + expect((err as { error?: any }).error).to.have.a.property('text'); + expect((err as { error?: any }).error.text).to.be.equal('Room "invalid-room" could not be found'); }) .end(done); }); describe('Send to a Public Channel', () => { - let publicMessageId; + let publicMessageId: IMessage['_id']; it('should send a message as app user', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-app-user`)) .set(credentials) .send({ @@ -59,8 +59,8 @@ describe('Apps - Send Messages As APP User', function () { }); }); describe('Send to a Private Channel', () => { - let privateMessageId; - let group; + let privateMessageId: IMessage['_id']; + let group: IRoom; before(async () => { group = ( @@ -74,7 +74,7 @@ describe('Apps - Send Messages As APP User', function () { after(() => deleteRoom({ type: 'p', roomId: group._id })); it('should send a message as app user', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-app-user`)) .set(credentials) .send({ @@ -94,8 +94,8 @@ describe('Apps - Send Messages As APP User', function () { }); }); describe('Send to a DM Channel', () => { - let DMMessageId; - let dmRoom; + let DMMessageId: IMessage['_id']; + let dmRoom: IRoom; before(async () => { dmRoom = ( @@ -109,7 +109,7 @@ describe('Apps - Send Messages As APP User', function () { after(() => deleteRoom({ type: 'd', roomId: dmRoom._id })); it('should send a message as app user', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-app-user`)) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.js b/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.ts similarity index 78% rename from apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.js rename to apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.ts index 61d812571d7d4..401a011cecea6 100644 --- a/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.js +++ b/apps/meteor/tests/end-to-end/apps/02-send-messages-as-user.ts @@ -1,17 +1,19 @@ +import type { Credentials } from '@rocket.chat/api-client'; +import type { App, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, request, credentials } from '../../data/api-data.js'; -import { apps } from '../../data/apps/apps-data.js'; -import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; -import { getMessageById } from '../../data/chat.helper.js'; +import { getCredentials, request, credentials } from '../../data/api-data'; +import { apps } from '../../data/apps/apps-data'; +import { cleanupApps, installTestApp } from '../../data/apps/helper'; +import { getMessageById } from '../../data/chat.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { adminUsername, password } from '../../data/user'; -import { createUser, deleteUser, login } from '../../data/users.helper.js'; +import type { TestUser } from '../../data/users.helper'; +import { createUser, deleteUser, login } from '../../data/users.helper'; -describe('Apps - Send Messages As User', function () { - this.retries(0); - let app; +describe('Apps - Send Messages As User', () => { + let app: App; before((done) => getCredentials(done)); before(async () => { @@ -23,41 +25,41 @@ describe('Apps - Send Messages As User', function () { describe('[Send Message as user]', () => { it('should return an error when the room is not found', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-user`)) .send({ roomId: 'invalid-room', }) .set(credentials) .expect(404) - .expect((err, res) => { + .expect((err: unknown, res: undefined) => { expect(err).to.have.a.property('error'); expect(res).to.be.equal(undefined); - expect(err.error).to.have.a.property('text'); - expect(err.error.text).to.be.equal('Room "invalid-room" could not be found'); + expect((err as { error?: any }).error).to.have.a.property('text'); + expect((err as { error?: any }).error.text).to.be.equal('Room "invalid-room" could not be found'); }) .end(done); }); it('should return an error when the user is not found', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-user?userId=invalid-user`)) .send({ roomId: 'GENERAL', }) .set(credentials) .expect(404) - .expect((err, res) => { + .expect((err: unknown, res: undefined) => { expect(err).to.have.a.property('error'); expect(res).to.be.equal(undefined); - expect(err.error).to.have.a.property('text'); - expect(err.error.text).to.be.equal('User with id "invalid-user" could not be found'); + expect((err as { error?: any }).error).to.have.a.property('text'); + expect((err as { error?: any }).error.text).to.be.equal('User with id "invalid-user" could not be found'); }) .end(done); }); describe('Send to a Public Channel', () => { - let publicMessageId; + let publicMessageId: IMessage['_id']; it('should send a message as app user', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-user?userId=${adminUsername}`)) .set(credentials) .send({ @@ -77,10 +79,10 @@ describe('Apps - Send Messages As User', function () { }); }); describe('Send to a Private Channel', () => { - let privateMessageId; - let group; - let user; - let userCredentials; + let privateMessageId: IMessage['_id']; + let group: IRoom; + let user: TestUser; + let userCredentials: Credentials; before(async () => { group = ( @@ -96,7 +98,7 @@ describe('Apps - Send Messages As User', function () { after(() => Promise.all([deleteRoom({ type: 'p', roomId: group._id }), deleteUser(user)])); it('should return 500 when sending a message as user that has no permissions', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-user?userId=${user._id}`)) .set(userCredentials) .send({ @@ -106,7 +108,7 @@ describe('Apps - Send Messages As User', function () { .end(done); }); it('should send a message as app user', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-user?userId=${adminUsername}`)) .set(credentials) .send({ @@ -126,8 +128,8 @@ describe('Apps - Send Messages As User', function () { }); }); describe('Send to a DM Channel', () => { - let DMMessageId; - let dmRoom; + let DMMessageId: IMessage['_id']; + let dmRoom: IRoom; before(async () => { dmRoom = ( @@ -141,7 +143,7 @@ describe('Apps - Send Messages As User', function () { after(() => deleteRoom({ type: 'd', roomId: dmRoom._id })); it('should send a message as app user', (done) => { - request + void request .post(apps(`/public/${app.id}/send-message-as-user?userId=${adminUsername}`)) .set(credentials) .send({ diff --git a/apps/meteor/tests/end-to-end/apps/03-slash-command-test-simple.js b/apps/meteor/tests/end-to-end/apps/03-slash-command-test-simple.ts similarity index 84% rename from apps/meteor/tests/end-to-end/apps/03-slash-command-test-simple.js rename to apps/meteor/tests/end-to-end/apps/03-slash-command-test-simple.ts index 64f25778fac2f..c74bcffaa4a2b 100644 --- a/apps/meteor/tests/end-to-end/apps/03-slash-command-test-simple.js +++ b/apps/meteor/tests/end-to-end/apps/03-slash-command-test-simple.ts @@ -1,12 +1,11 @@ +import type { IMessage } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, request, credentials, api } from '../../data/api-data.js'; -import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; - -describe('Apps - Slash Command "test-simple"', function () { - this.retries(0); +import { getCredentials, request, credentials, api } from '../../data/api-data'; +import { cleanupApps, installTestApp } from '../../data/apps/helper'; +describe('Apps - Slash Command "test-simple"', () => { before((done) => getCredentials(done)); before(async () => { await cleanupApps(); @@ -17,7 +16,7 @@ describe('Apps - Slash Command "test-simple"', function () { describe('[Slash command "test-simple"]', () => { it('should return an error when no command is provided', (done) => { - request + void request .post(api('commands.run')) .send({ roomId: 'GENERAL', @@ -32,7 +31,7 @@ describe('Apps - Slash Command "test-simple"', function () { .end(done); }); it('should return an error when the command does not exist', (done) => { - request + void request .post(api('commands.run')) .send({ roomId: 'GENERAL', @@ -47,7 +46,7 @@ describe('Apps - Slash Command "test-simple"', function () { .end(done); }); it('should execute the slash command successfully', (done) => { - request + void request .post(api('commands.run')) .send({ roomId: 'GENERAL', @@ -61,7 +60,7 @@ describe('Apps - Slash Command "test-simple"', function () { .end(done); }); it('should have sent the message correctly', (done) => { - request + void request .get(api('chat.search')) .query({ roomId: 'GENERAL', @@ -70,7 +69,9 @@ describe('Apps - Slash Command "test-simple"', function () { .set(credentials) .expect(200) .expect((res) => { - const message = res.body.messages.find((message) => message.msg === "Slashcommand 'test-simple' successfully executed"); + const message = (res.body.messages as IMessage[]).find( + (message) => message.msg === "Slashcommand 'test-simple' successfully executed", + ); expect(message).to.not.be.equal(undefined); }) .end(done); diff --git a/apps/meteor/tests/end-to-end/apps/04-slash-command-test-with-arguments.js b/apps/meteor/tests/end-to-end/apps/04-slash-command-test-with-arguments.ts similarity index 81% rename from apps/meteor/tests/end-to-end/apps/04-slash-command-test-with-arguments.js rename to apps/meteor/tests/end-to-end/apps/04-slash-command-test-with-arguments.ts index 58ee4ab0d9c85..4701ed3c4c85e 100644 --- a/apps/meteor/tests/end-to-end/apps/04-slash-command-test-with-arguments.js +++ b/apps/meteor/tests/end-to-end/apps/04-slash-command-test-with-arguments.ts @@ -1,12 +1,11 @@ +import type { IMessage } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, request, credentials, api } from '../../data/api-data.js'; -import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; - -describe('Apps - Slash Command "test-with-arguments"', function () { - this.retries(0); +import { getCredentials, request, credentials, api } from '../../data/api-data'; +import { cleanupApps, installTestApp } from '../../data/apps/helper'; +describe('Apps - Slash Command "test-with-arguments"', () => { before((done) => getCredentials(done)); before(async () => { await cleanupApps(); @@ -18,7 +17,7 @@ describe('Apps - Slash Command "test-with-arguments"', function () { describe('[Slash command "test-with-arguments"]', () => { const params = 'argument'; it('should execute the slash command successfully', (done) => { - request + void request .post(api('commands.run')) .send({ roomId: 'GENERAL', @@ -34,7 +33,7 @@ describe('Apps - Slash Command "test-with-arguments"', function () { }); it('should have sent the message correctly', (done) => { const searchText = `Slashcommand \'test-with-arguments\' successfully executed with arguments: "${params}"`; - request + void request .get(api('chat.search')) .query({ roomId: 'GENERAL', @@ -43,7 +42,7 @@ describe('Apps - Slash Command "test-with-arguments"', function () { .set(credentials) .expect(200) .expect((res) => { - const message = res.body.messages.find((message) => message.msg === searchText); + const message = (res.body.messages as IMessage[]).find((message) => message.msg === searchText); expect(message).to.not.be.equal(undefined); }) .end(done); diff --git a/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts index d7772b7a233a7..8d29eee97b7d7 100644 --- a/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts +++ b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts @@ -2,15 +2,13 @@ import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; -import { getCredentials, request, api, credentials } from '../../data/api-data.js'; -import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; +import { getCredentials, request, api, credentials } from '../../data/api-data'; +import { cleanupApps, installTestApp } from '../../data/apps/helper'; import { updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { adminUsername } from '../../data/user'; -describe('Apps - Video Conferences', function () { - this.retries(0); - +describe('Apps - Video Conferences', () => { before((done) => getCredentials(done)); const roomName = `apps-e2etest-room-${Date.now()}-videoconf`; diff --git a/apps/meteor/tests/end-to-end/apps/apps-uninstall.js b/apps/meteor/tests/end-to-end/apps/apps-uninstall.ts similarity index 85% rename from apps/meteor/tests/end-to-end/apps/apps-uninstall.js rename to apps/meteor/tests/end-to-end/apps/apps-uninstall.ts index 15130c79e1291..e51bbb31bd96c 100644 --- a/apps/meteor/tests/end-to-end/apps/apps-uninstall.js +++ b/apps/meteor/tests/end-to-end/apps/apps-uninstall.ts @@ -1,13 +1,13 @@ +import type { App } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, request, credentials } from '../../data/api-data.js'; -import { apps } from '../../data/apps/apps-data.js'; -import { installTestApp, cleanupApps } from '../../data/apps/helper.js'; +import { getCredentials, request, credentials } from '../../data/api-data'; +import { apps } from '../../data/apps/apps-data'; +import { installTestApp, cleanupApps } from '../../data/apps/helper'; -describe('Apps - Uninstall', function () { - this.retries(0); - let app; +describe('Apps - Uninstall', () => { + let app: App; before((done) => getCredentials(done)); @@ -20,7 +20,7 @@ describe('Apps - Uninstall', function () { describe('[Uninstall]', () => { it('should throw an error when trying to uninstall an invalid app', (done) => { - request + void request .delete(apps('/invalid-id')) .set(credentials) .expect('Content-Type', 'application/json') @@ -32,7 +32,7 @@ describe('Apps - Uninstall', function () { .end(done); }); it('should remove the app successfully', (done) => { - request + void request .delete(apps(`/${app.id}`)) .set(credentials) .expect('Content-Type', 'application/json') diff --git a/apps/meteor/tests/end-to-end/teardown.js b/apps/meteor/tests/end-to-end/teardown.ts similarity index 59% rename from apps/meteor/tests/end-to-end/teardown.js rename to apps/meteor/tests/end-to-end/teardown.ts index 0cf40a45e5048..7ad3917a985c6 100644 --- a/apps/meteor/tests/end-to-end/teardown.js +++ b/apps/meteor/tests/end-to-end/teardown.ts @@ -1,12 +1,13 @@ import { afterEach } from 'mocha'; +import type { Response } from 'supertest'; -import { request } from '../data/api-data.js'; +import { request } from '../data/api-data'; -const methods = ['get', 'post', 'put', 'del']; +const methods = ['get', 'post', 'put', 'del'] as const; -let lastUrl; -let lastMethod; -let lastResponse; +let lastUrl: string; +let lastMethod: string; +let lastResponse: Response; methods.forEach((method) => { const original = request[method]; @@ -20,7 +21,7 @@ methods.forEach((method) => { }); afterEach(async function () { - if (this.currentTest.state === 'failed') { + if (this.currentTest?.state === 'failed') { console.log({ lastUrl, lastMethod, diff --git a/apps/meteor/tests/unit/app/markdown/client.tests.js b/apps/meteor/tests/unit/app/markdown/client.tests.js index cf970b1822e60..7b32fae7e4bf1 100644 --- a/apps/meteor/tests/unit/app/markdown/client.tests.js +++ b/apps/meteor/tests/unit/app/markdown/client.tests.js @@ -1,7 +1,7 @@ import { escapeHTML } from '@rocket.chat/string-helpers'; import { expect } from 'chai'; -import { Markdown, original, filtered } from './client.mocks.js'; +import { Markdown, original, filtered } from './client.mocks'; const wrapper = (text, tag) => `${tag}${text}${tag}`; const boldWrapper = (text) => wrapper(`${text}`, '*'); diff --git a/ee/packages/api-client/src/Credentials.ts b/ee/packages/api-client/src/Credentials.ts new file mode 100644 index 0000000000000..721c9aa5a90cc --- /dev/null +++ b/ee/packages/api-client/src/Credentials.ts @@ -0,0 +1,4 @@ +export type Credentials = { + 'X-User-Id': string; + 'X-Auth-Token': string; +}; diff --git a/ee/packages/api-client/src/index.ts b/ee/packages/api-client/src/index.ts index 7a664b9fcc8a8..a11e032e91b29 100644 --- a/ee/packages/api-client/src/index.ts +++ b/ee/packages/api-client/src/index.ts @@ -9,10 +9,11 @@ import type { } from '@rocket.chat/rest-typings'; import { stringify } from 'query-string'; +import type { Credentials } from './Credentials'; import type { Middleware, RestClientInterface } from './RestClientInterface'; import { hasRequiredTwoFactorMethod, isTotpInvalidError, isTotpRequiredError } from './errors'; -export { RestClientInterface }; +export { RestClientInterface, Credentials }; const pipe = any>(fn: T) => @@ -60,25 +61,9 @@ export class RestClient implements RestClientInterface { private headers: Record = {}; - private credentials: - | { - 'X-User-Id': string; - 'X-Auth-Token': string; - } - | undefined; - - constructor({ - baseUrl, - credentials, - headers = {}, - }: { - baseUrl: string; - credentials?: { - 'X-User-Id': string; - 'X-Auth-Token': string; - }; - headers?: Record; - }) { + private credentials: Credentials | undefined; + + constructor({ baseUrl, credentials, headers = {} }: { baseUrl: string; credentials?: Credentials; headers?: Record }) { this.baseUrl = `${baseUrl}/api`; this.setCredentials(credentials); this.headers = headers; diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 74d4ca2ef4876..accc78585c29d 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -94,8 +94,6 @@ export interface IRoom extends IRocketChatRecord { /* @deprecated */ customFields?: Record; - channel?: { _id: string }; - usersWaitingForE2EKeys?: { userId: IUser['_id']; ts: Date }[]; } diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index d7de094b90c19..136146e40a34c 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -152,7 +152,6 @@ export interface IUser extends IRocketChatRecord { private_key: string; public_key: string; }; - requirePasswordChange?: boolean; customFields?: { [key: string]: any; }; @@ -184,6 +183,8 @@ export interface IUser extends IRocketChatRecord { }; importIds?: string[]; _pendingAvatarUrl?: string; + requirePasswordChange?: boolean; + requirePasswordChangeReason?: string; } export interface IRegisterUser extends IUser { diff --git a/packages/password-policies/package.json b/packages/password-policies/package.json index 920554bad2032..4b2f0c76b7ae5 100644 --- a/packages/password-policies/package.json +++ b/packages/password-policies/package.json @@ -3,7 +3,7 @@ "version": "0.0.2", "private": true, "devDependencies": { - "@types/chai": "^4.3.9", + "@types/chai": "~4.3.16", "@types/jest": "~29.5.7", "chai": "^4.3.10", "eslint": "~8.45.0", diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index ee05f10f2c14b..1c0b6a360f7b6 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -575,7 +575,7 @@ export type RoomsEndpoints = { '/v1/rooms.createDiscussion': { POST: (params: RoomsCreateDiscussionProps) => { - discussion: IRoom; + discussion: IRoom & { rid: IRoom['_id'] }; }; }; diff --git a/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx b/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx index 15290a823d4af..daf66606f98f4 100644 --- a/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx +++ b/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx @@ -1,3 +1,4 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { Button, FieldGroup, Field, FieldLabel, ButtonGroup, PasswordInput, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form } from '@rocket.chat/layout'; @@ -13,7 +14,7 @@ import HorizontalTemplate from '../template/HorizontalTemplate'; const getChangePasswordReason = ({ requirePasswordChange, requirePasswordChangeReason = requirePasswordChange ? 'You_need_to_change_your_password' : 'Please_enter_your_new_password_below', -}: { requirePasswordChange?: boolean; requirePasswordChangeReason?: TranslationKey } = {}): TranslationKey => requirePasswordChangeReason; +}: Pick = {}) => requirePasswordChangeReason as TranslationKey; const ResetPasswordPage = (): ReactElement => { const user = useUser(); diff --git a/yarn.lock b/yarn.lock index 97fe3c5151d51..116c715d3c600 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9436,7 +9436,7 @@ __metadata: "@types/bcrypt": ^5.0.1 "@types/body-parser": ^1.19.4 "@types/busboy": ^1.5.2 - "@types/chai": ^4.3.9 + "@types/chai": ~4.3.16 "@types/chai-as-promised": ^7.1.7 "@types/chai-datetime": 0.0.38 "@types/chai-dom": 1.11.2 @@ -9870,7 +9870,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/password-policies@workspace:packages/password-policies" dependencies: - "@types/chai": ^4.3.9 + "@types/chai": ~4.3.16 "@types/jest": ~29.5.7 chai: ^4.3.10 eslint: ~8.45.0 @@ -12994,10 +12994,10 @@ __metadata: languageName: node linkType: hard -"@types/chai@npm:*, @types/chai@npm:^4.3.9": - version: 4.3.9 - resolution: "@types/chai@npm:4.3.9" - checksum: 2300a2c7abd4cb590349927a759b3d0172211a69f363db06e585faf7874a47f125ef3b364cce4f6190e3668147587fc11164c791c9560cf9bce8478fb7019610 +"@types/chai@npm:*, @types/chai@npm:~4.3.16": + version: 4.3.16 + resolution: "@types/chai@npm:4.3.16" + checksum: bb5f52d1b70534ed8b4bf74bd248add003ffe1156303802ea367331607c06b494da885ffbc2b674a66b4f90c9ee88759790a5f243879f6759f124f22328f5e95 languageName: node linkType: hard @@ -18321,9 +18321,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001541": - version: 1.0.30001577 - resolution: "caniuse-lite@npm:1.0.30001577" - checksum: 26d2b4a498a2a6ad5a33c44c18a32497b59a3bb1963b8b9221ddcbfe166ed7f7a1f75a3de040870cdc2467ce35199c643cfe8c45e7208d8bc033e7877214b0f9 + version: 1.0.30001636 + resolution: "caniuse-lite@npm:1.0.30001636" + checksum: b0347fd2c8d346680a64d98b061c59cb8fbf149cdd03005a447fae4d21e6286d5bd161b43eefe3221c6624aacb3cda4e838ae83c95ff5313a547f84ca93bcc70 languageName: node linkType: hard From 209a0628154413eb5ff11e20b3b63e9363bbb372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 24 Jun 2024 19:23:17 -0300 Subject: [PATCH 005/114] test: improve selectors specificity (#32665) --- apps/meteor/client/components/FilterByText.tsx | 1 + .../client/views/omnichannel/managers/AddManager.tsx | 8 +++++--- apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts | 2 +- .../tests/e2e/page-objects/omnichannel-business-hours.ts | 2 +- .../tests/e2e/page-objects/omnichannel-custom-fields.ts | 2 +- .../tests/e2e/page-objects/omnichannel-departments.ts | 2 +- apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts | 4 ++-- .../meteor/tests/e2e/page-objects/omnichannel-monitors.ts | 2 +- .../tests/e2e/page-objects/omnichannel-sla-policies.ts | 2 +- apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts | 2 +- apps/meteor/tests/e2e/page-objects/omnichannel-units.ts | 2 +- 11 files changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/meteor/client/components/FilterByText.tsx b/apps/meteor/client/components/FilterByText.tsx index 0f317dea61e4d..1aeeb29a0a57a 100644 --- a/apps/meteor/client/components/FilterByText.tsx +++ b/apps/meteor/client/components/FilterByText.tsx @@ -54,6 +54,7 @@ const FilterByText = forwardRef(function Fi value={text} flexGrow={2} minWidth='x220' + aria-label={placeholder ?? t('Search')} /> {isFilterByTextPropsWithButton(props) ? ( diff --git a/apps/meteor/client/views/omnichannel/managers/AddManager.tsx b/apps/meteor/client/views/omnichannel/managers/AddManager.tsx index b4f56f78b62bb..e21896eef9fa1 100644 --- a/apps/meteor/client/views/omnichannel/managers/AddManager.tsx +++ b/apps/meteor/client/views/omnichannel/managers/AddManager.tsx @@ -1,5 +1,5 @@ import { Button, Box, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; @@ -12,6 +12,8 @@ const AddManager = ({ reload }: { reload: () => void }): ReactElement => { const [username, setUsername] = useState(''); const dispatchToastMessage = useToastMessageDispatch(); + const usernameFieldId = useUniqueId(); + const saveAction = useEndpointAction('POST', '/v1/livechat/users/manager'); const handleSave = useMutableCallback(async () => { @@ -34,9 +36,9 @@ const AddManager = ({ reload }: { reload: () => void }): ReactElement => { return ( - {t('Username')} + {t('Username')} - + diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts index 63818a06032ae..d588e409423ff 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts @@ -23,7 +23,7 @@ export class OmnichannelAgents { } get inputSearch(): Locator { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } get btnAdd(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts index 19e93cefb307a..a77784c4538ec 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts @@ -20,7 +20,7 @@ export class OmnichannelBusinessHours extends OmnichannelAdministration { } get inputSearch(): Locator { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } get inputName(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts index 9768139fe09d3..e3fd6e382b9fa 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts @@ -33,7 +33,7 @@ export class OmnichannelCustomFields { } get inputSearch(): Locator { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } firstRowInTable(filedName: string) { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts index c1012442f1159..a41332122a363 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts @@ -13,7 +13,7 @@ export class OmnichannelDepartments { } get inputSearch() { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } async search(text: string) { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts index 0f2e9ac516e79..f52aa66fda629 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts @@ -13,7 +13,7 @@ export class OmnichannelManager { } private get inputSearch() { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } async search(text: string) { @@ -27,7 +27,7 @@ export class OmnichannelManager { } get inputUsername(): Locator { - return this.page.locator('input').first(); + return this.page.getByRole('main').getByLabel('Username'); } async selectUsername(username: string) { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts index b72594024aaf3..e31878842ddb4 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts @@ -20,7 +20,7 @@ export class OmnichannelMonitors extends OmnichannelAdministration { } get inputSearch(): Locator { - return this.page.locator('input[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } findRowByName(name: string): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts index d44d4d0f37b46..185639569d803 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts @@ -60,7 +60,7 @@ export class OmnichannelSlaPolicies { } get inputSearch() { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } headingButtonNew(name: string) { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts index afb5775ba1f56..f283473505cf2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts @@ -24,7 +24,7 @@ export class OmnichannelTags extends OmnichannelAdministration { } get inputSearch(): Locator { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } get confirmDeleteModal(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts index f5d02e2150c71..abcd794f8efa9 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts @@ -4,7 +4,7 @@ import { OmnichannelAdministration } from './omnichannel-administration'; export class OmnichannelUnits extends OmnichannelAdministration { get inputSearch() { - return this.page.locator('[placeholder="Search"]'); + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); } async search(text: string) { From 24f7df489437aa9f92159caac50d6917e0bdcb0e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 25 Jun 2024 18:11:41 -0300 Subject: [PATCH 006/114] feat(apps-engine): add `customFields` on livechat creation (#32328) --- .changeset/rare-penguins-hope.md | 6 ++ .../app/apps/server/bridges/livechat.ts | 32 ++++--- apps/meteor/app/apps/server/bridges/rooms.ts | 8 ++ .../app/livechat/server/api/lib/livechat.ts | 11 ++- apps/meteor/app/livechat/server/lib/Helper.ts | 73 ++++++++------- .../app/livechat/server/lib/LivechatTyped.ts | 11 ++- .../app/livechat/server/lib/QueueManager.ts | 20 ++++- .../server/hooks/beforeNewInquiry.ts | 90 +++++++++---------- apps/meteor/lib/callbacks.ts | 12 ++- packages/core-typings/src/IRoom.ts | 4 +- 10 files changed, 160 insertions(+), 107 deletions(-) create mode 100644 .changeset/rare-penguins-hope.md diff --git a/.changeset/rare-penguins-hope.md b/.changeset/rare-penguins-hope.md new file mode 100644 index 0000000000000..187bd9d09ddcf --- /dev/null +++ b/.changeset/rare-penguins-hope.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +--- + +Allow customFields on livechat creation bridge diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 7067ab8e6a52f..c2a987148864b 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -15,6 +15,12 @@ import { getRoom } from '../../../livechat/server/api/lib/livechat'; import { type ILivechatMessage, Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { settings } from '../../../settings/server'; +declare module '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator' { + interface IExtraRoomParams { + customFields?: Record; + } +} + export class AppLivechatBridge extends LivechatBridge { constructor(private readonly orch: IAppServerOrchestrator) { super(); @@ -79,17 +85,14 @@ export class AppLivechatBridge extends LivechatBridge { await LivechatTyped.updateMessage(data); } - protected async createRoom(visitor: IVisitor, agent: IUser, appId: string, extraParams?: IExtraRoomParams): Promise { + protected async createRoom( + visitor: IVisitor, + agent: IUser, + appId: string, + { source, customFields }: IExtraRoomParams = {}, + ): Promise { this.orch.debugLog(`The App ${appId} is creating a livechat room.`); - const { source } = extraParams || {}; - // `source` will likely have the properties below, so we tell TS it's alright - const { sidebarIcon, defaultIcon, label } = (source || {}) as { - sidebarIcon?: string; - defaultIcon?: string; - label?: string; - }; - let agentRoom: SelectedAgent | undefined; if (agent?.id) { const user = await Users.getAgentInfo(agent.id, settings.get('Livechat_show_agent_email')); @@ -108,12 +111,15 @@ export class AppLivechatBridge extends LivechatBridge { type: OmnichannelSourceType.APP, id: appId, alias: this.orch.getManager()?.getOneById(appId)?.getName(), - label, - sidebarIcon, - defaultIcon, + ...(source && + source.type === 'app' && { + sidebarIcon: source.sidebarIcon, + defaultIcon: source.defaultIcon, + label: source.label, + }), }, }, - extraParams: undefined, + extraParams: customFields && { customFields }, }); // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index bbd24152716f0..1584d07886297 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -209,4 +209,12 @@ export class AppRoomBridge extends RoomBridge { const userConverter = this.orch.getConverters().get('users'); return users.map((user: ICoreUser) => userConverter.convertToApp(user)); } + + protected getMessages( + _roomId: string, + _options: { limit: number; skip?: number; sort?: Record }, + _appId: string, + ): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index 00229dae2de51..e05f119fc46e3 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -5,6 +5,7 @@ import type { ILivechatVisitor, IOmnichannelRoom, SelectedAgent, + OmnichannelSourceType, } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { EmojiCustom, LivechatTrigger, LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; @@ -104,7 +105,13 @@ export async function findOpenRoom(token: string, departmentId?: string): Promis return rooms[0]; } } -export function getRoom({ +export function getRoom< + E extends Record & { + sla?: string; + customFields?: Record; + source?: OmnichannelSourceType; + }, +>({ guest, rid, roomInfo, @@ -117,7 +124,7 @@ export function getRoom({ source?: IOmnichannelRoom['source']; }; agent?: SelectedAgent; - extraParams?: Record; + extraParams?: E; }): Promise<{ room: IOmnichannelRoom; newRoom: boolean }> { const token = guest?.token; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index dacd99be00f92..6663120f7b674 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -57,12 +57,18 @@ export const allowAgentSkipQueue = (agent: SelectedAgent) => { return hasRoleAsync(agent.agentId, 'bot'); }; -export const createLivechatRoom = async ( +export const createLivechatRoom = async < + E extends Record & { + sla?: string; + customFields?: Record; + source?: OmnichannelSourceType; + }, +>( rid: string, name: string, guest: ILivechatVisitor, roomInfo: Partial = {}, - extraData = {}, + extraData?: E, ) => { check(rid, String); check(name, String); @@ -86,39 +92,38 @@ export const createLivechatRoom = async ( visitor: { _id, username, departmentId, status, activity }, }); - const room: InsertionModel = Object.assign( - { - _id: rid, - msgs: 0, - usersCount: 1, - lm: newRoomAt, - fname: name, - t: 'l' as const, - ts: newRoomAt, - departmentId, - v: { - _id, - username, - token, - status, - ...(activity?.length && { activity }), - }, - cl: false, - open: true, - waitingResponse: true, - // this should be overriden by extraRoomInfo when provided - // in case it's not provided, we'll use this "default" type - source: { - type: OmnichannelSourceType.OTHER, - alias: 'unknown', - }, - queuedAt: newRoomAt, - - priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED, - estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, + // TODO: Solve `u` missing issue + const room: InsertionModel = { + _id: rid, + msgs: 0, + usersCount: 1, + lm: newRoomAt, + fname: name, + t: 'l' as const, + ts: newRoomAt, + departmentId, + v: { + _id, + username, + token, + status, + ...(activity?.length && { activity }), }, - extraRoomInfo, - ); + cl: false, + open: true, + waitingResponse: true, + // this should be overridden by extraRoomInfo when provided + // in case it's not provided, we'll use this "default" type + source: { + type: OmnichannelSourceType.OTHER, + alias: 'unknown', + }, + queuedAt: newRoomAt, + livechatData: undefined, + priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED, + estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, + ...extraRoomInfo, + } as InsertionModel; const roomId = (await Rooms.insertOne(room)).insertedId; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index bf5014b984f18..c22b984ffe3a2 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -21,6 +21,7 @@ import type { IOmnichannelAgent, ILivechatDepartmentAgents, LivechatDepartmentDTO, + OmnichannelSourceType, } from '@rocket.chat/core-typings'; import { ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; import { Logger, type MainLogger } from '@rocket.chat/logger'; @@ -383,7 +384,13 @@ class LivechatClass { } } - async getRoom( + async getRoom< + E extends Record & { + sla?: string; + customFields?: Record; + source?: OmnichannelSourceType; + }, + >( guest: ILivechatVisitor, message: Pick, roomInfo: { @@ -391,7 +398,7 @@ class LivechatClass { [key: string]: unknown; }, agent?: SelectedAgent, - extraData?: Record, + extraData?: E, ) { if (!this.enabled()) { throw new Meteor.Error('error-omnichannel-is-disabled'); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 576b29990b33a..5b1fbb21b9a85 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -7,6 +7,7 @@ import { type IMessage, type IOmnichannelRoom, type SelectedAgent, + type OmnichannelSourceType, } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { LivechatInquiry, LivechatRooms, Users } from '@rocket.chat/models'; @@ -65,7 +66,13 @@ export const queueInquiry = async (inquiry: ILivechatInquiryRecord, defaultAgent }; type queueManager = { - requestRoom: (params: { + requestRoom: < + E extends Record & { + sla?: string; + customFields?: Record; + source?: OmnichannelSourceType; + }, + >(params: { guest: ILivechatVisitor; message: Pick; roomInfo: { @@ -73,13 +80,13 @@ type queueManager = { [key: string]: unknown; }; agent?: SelectedAgent; - extraData?: Record; + extraData?: E; }) => Promise; unarchiveRoom: (archivedRoom?: IOmnichannelRoom) => Promise; }; export const QueueManager: queueManager = { - async requestRoom({ guest, message, roomInfo, agent, extraData }) { + async requestRoom({ guest, message, roomInfo, agent, extraData: { customFields, ...extraData } = {} }) { logger.debug(`Requesting a room for guest ${guest._id}`); check( message, @@ -106,7 +113,12 @@ export const QueueManager: queueManager = { const { rid } = message; const name = (roomInfo?.fname as string) || guest.name || guest.username; - const room = await LivechatRooms.findOneById(await createLivechatRoom(rid, name, guest, roomInfo, extraData)); + const room = await LivechatRooms.findOneById( + await createLivechatRoom(rid, name, guest, roomInfo, { + ...(Boolean(customFields) && { customFields }), + ...extraData, + }), + ); if (!room) { logger.error(`Room for visitor ${guest._id} not found`); throw new Error('room-not-found'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts index e6d68df1aa965..21ee8c8b5e567 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeNewInquiry.ts @@ -4,55 +4,49 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; -type Props = { - sla?: string; - priority?: string; - [other: string]: any; -}; - -const beforeNewInquiry = async (extraData: Props) => { - const { sla: slaSearchTerm, priority: prioritySearchTerm, ...props } = extraData; - if (!slaSearchTerm && !prioritySearchTerm) { - return extraData; - } - - let sla: IOmnichannelServiceLevelAgreements | null = null; - let priority: ILivechatPriority | null = null; - - if (slaSearchTerm) { - sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(slaSearchTerm, { - projection: { dueTimeInMinutes: 1 }, - }); - if (!sla) { - throw new Meteor.Error('error-invalid-sla', 'Invalid sla', { - function: 'livechat.beforeInquiry', +callbacks.add( + 'livechat.beforeInquiry', + async (extraData) => { + const { sla: slaSearchTerm, priority: prioritySearchTerm, ...props } = extraData; + if (!slaSearchTerm && !prioritySearchTerm) { + return extraData; + } + let sla: IOmnichannelServiceLevelAgreements | null = null; + let priority: ILivechatPriority | null = null; + if (slaSearchTerm) { + sla = await OmnichannelServiceLevelAgreements.findOneByIdOrName(slaSearchTerm, { + projection: { dueTimeInMinutes: 1 }, }); + if (!sla) { + throw new Meteor.Error('error-invalid-sla', 'Invalid sla', { + function: 'livechat.beforeInquiry', + }); + } } - } - if (prioritySearchTerm) { - priority = await LivechatPriority.findOneByIdOrName(prioritySearchTerm, { - projection: { _id: 1, sortItem: 1 }, - }); - if (!priority) { - throw new Meteor.Error('error-invalid-priority', 'Invalid priority', { - function: 'livechat.beforeInquiry', + if (prioritySearchTerm) { + priority = await LivechatPriority.findOneByIdOrName(prioritySearchTerm, { + projection: { _id: 1, sortItem: 1 }, }); + if (!priority) { + throw new Meteor.Error('error-invalid-priority', 'Invalid priority', { + function: 'livechat.beforeInquiry', + }); + } } - } - - const ts = new Date(); - const changes: Partial = { - ts, - }; - if (sla) { - changes.slaId = sla._id; - changes.estimatedWaitingTimeQueue = sla.dueTimeInMinutes; - } - if (priority) { - changes.priorityId = priority._id; - changes.priorityWeight = priority.sortItem; - } - return { ...props, ...changes }; -}; - -callbacks.add('livechat.beforeInquiry', beforeNewInquiry, callbacks.priority.MEDIUM, 'livechat-before-new-inquiry'); + const ts = new Date(); + const changes: Partial = { + ts, + }; + if (sla) { + changes.slaId = sla._id; + changes.estimatedWaitingTimeQueue = sla.dueTimeInMinutes; + } + if (priority) { + changes.priorityId = priority._id; + changes.priorityWeight = priority.sortItem; + } + return { ...props, ...changes }; + }, + callbacks.priority.MEDIUM, + 'livechat-before-new-inquiry', +); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 9b5f1e69b1d6f..e721e9014d819 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -21,6 +21,7 @@ import type { UserStatus, ILivechatDepartment, MessageMention, + OmnichannelSourceType, } from '@rocket.chat/core-typings'; import type { FilterOperators } from 'mongodb'; @@ -199,7 +200,15 @@ type ChainedCallbackSignatures = { options: { forwardingToDepartment?: { oldDepartmentId?: string; transferData?: any }; clientAction?: boolean }; }, ) => Promise<(IOmnichannelRoom & { chatQueued: boolean }) | undefined>; - 'livechat.beforeInquiry': (data: Pick) => Pick; + 'livechat.beforeInquiry': ( + data: Pick & { sla?: string; priority?: string; [other: string]: unknown } & { + customFields?: Record; + source?: OmnichannelSourceType; + }, + ) => Pick & { sla?: string; priority?: string; [other: string]: unknown } & { + customFields?: Record; + source?: OmnichannelSourceType; + }; 'roomNameChanged': (room: IRoom) => void; 'roomTopicChanged': (room: IRoom) => void; 'roomAnnouncementChanged': (room: IRoom) => void; @@ -223,7 +232,6 @@ export type Hook = | 'beforeRemoveFromRoom' | 'beforeValidateLogin' | 'livechat.beforeForwardRoomToDepartment' - | 'livechat.beforeRoom' | 'livechat.beforeRouteChat' | 'livechat.chatQueued' | 'livechat.checkAgentBeforeTakeInquiry' diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index accc78585c29d..ac31c2cc6a3ae 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -150,7 +150,7 @@ export enum OmnichannelSourceType { OTHER = 'other', // catch-all source type } -export interface IOmnichannelGenericRoom extends Omit { +export interface IOmnichannelGenericRoom extends Omit { t: 'l' | 'v'; v: Pick & { lastMessageTs?: Date; @@ -331,7 +331,7 @@ export const isOmnichannelRoom = (room: Pick): room is IOmnichannelR export const isVoipRoom = (room: IRoom): room is IVoipRoom & IRoom => room.t === 'v'; -export const isOmnichannelRoomFromAppSource = (room: IRoom): room is IOmnichannelRoomFromAppSource => { +export const isOmnichannelRoomFromAppSource = (room: IOmnichannelRoom): room is IOmnichannelRoomFromAppSource => { if (!isOmnichannelRoom(room)) { return false; } From c3489db38c0c9e89a3f47353f94352f2583e1a6a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 25 Jun 2024 20:54:10 -0300 Subject: [PATCH 007/114] chore: fix uikit `PreviewBlock` typings (#32676) --- packages/ui-kit/src/blocks/layout/PreviewBlock.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui-kit/src/blocks/layout/PreviewBlock.ts b/packages/ui-kit/src/blocks/layout/PreviewBlock.ts index 308e804d9919e..f46e11ccafb00 100644 --- a/packages/ui-kit/src/blocks/layout/PreviewBlock.ts +++ b/packages/ui-kit/src/blocks/layout/PreviewBlock.ts @@ -1,4 +1,5 @@ import type { LayoutBlockType } from '../LayoutBlockType'; +import type { LayoutBlockish } from '../LayoutBlockish'; import type { TextObject } from '../TextObject'; import type { ContextBlock } from './ContextBlock'; @@ -28,7 +29,7 @@ export type PreviewBlockWithPreview = PreviewBlockBase & { thumb: undefined; }; -export type PreviewBlock = PreviewBlockBase | PreviewBlockWithThumb | PreviewBlockWithPreview; +export type PreviewBlock = LayoutBlockish; export const isPreviewBlockWithThumb = (previewBlock: PreviewBlock): previewBlock is PreviewBlockWithThumb => 'thumb' in previewBlock; From dbc79b76164c211eda3cf47b74a6aa94d8831abe Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 26 Jun 2024 18:46:34 -0300 Subject: [PATCH 008/114] fix(Livechat): Restore missing setDepartment livechat API endpoint functionality (#32626) --- .changeset/funny-snails-promise.md | 10 ++ .../omnichannel-livechat-api.spec.ts | 116 +++++++++++++----- .../omnichannel-livechat-embedded.ts | 6 +- .../components/Form/CustomFields/index.tsx | 3 +- .../src/components/Form/SelectInput/index.tsx | 19 +-- packages/livechat/src/lib/hooks.ts | 35 +++--- .../livechat/src/routes/Chat/container.js | 8 ++ .../livechat/src/routes/Register/index.tsx | 40 +++--- packages/livechat/src/store/index.tsx | 1 - 9 files changed, 148 insertions(+), 90 deletions(-) create mode 100644 .changeset/funny-snails-promise.md diff --git a/.changeset/funny-snails-promise.md b/.changeset/funny-snails-promise.md new file mode 100644 index 0000000000000..bdd74a60b1e90 --- /dev/null +++ b/.changeset/funny-snails-promise.md @@ -0,0 +1,10 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/livechat": patch +--- + +livechat `setDepartment` livechat api fixes: +- Changing department didn't reflect on the registration form in real time +- Changing the department mid conversation didn't transfer the chat +- Depending on the state of the department, it couldn't be set as default + diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts index bf77840446dc5..278df11f16cb8 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts @@ -212,7 +212,6 @@ test.describe('OC - Livechat API', () => { test.skip(!IS_EE, 'Enterprise Only'); // Tests that requires interaction from an agent or more let poAuxContext: { page: Page; poHomeOmnichannel: HomeOmnichannel }; - let poAuxContext2: { page: Page; poHomeOmnichannel: HomeOmnichannel }; let poLiveChat: OmnichannelLiveChatEmbedded; let page: Page; let agent: Awaited>; @@ -247,18 +246,12 @@ test.describe('OC - Livechat API', () => { await poAuxContext.poHomeOmnichannel.sidenav.switchStatus('online'); } - if (testInfo.title === 'OC - Livechat API - setDepartment') { - const { page: pageCtx2 } = await createAuxContext(browser, Users.user2); - poAuxContext2 = { page: pageCtx2, poHomeOmnichannel: new HomeOmnichannel(pageCtx) }; - } - await page.goto('/packages/rocketchat_livechat/assets/demo.html'); }); test.afterEach(async () => { await poAuxContext.page.close(); await page.close(); - await poAuxContext2?.page.close(); await pageContext?.close(); }); @@ -316,41 +309,100 @@ test.describe('OC - Livechat API', () => { }); }); - test('OC - Livechat API - setDepartment', async () => { - const [departmentA, departmentB] = departments.map(({ data }) => data); - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - department: departmentA._id, - }; + test.describe('OC - Livechat API - setDepartment', () => { + let poAuxContext2: { page: Page; poHomeOmnichannel: HomeOmnichannel }; - // Start Chat - await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); - await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).toBeVisible(); + test.beforeEach(async ({ browser }) => { + const { page: pageCtx2 } = await createAuxContext(browser, Users.user2); + poAuxContext2 = { page: pageCtx2, poHomeOmnichannel: new HomeOmnichannel(pageCtx2) }; + }); - await poLiveChat.page.evaluate( - (registerGuestVisitor) => window.RocketChat.livechat.registerGuest(registerGuestVisitor), - registerGuestVisitor, - ); + test.afterEach(async () => { + await poAuxContext2.page.close(); + }); - await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).not.toBeVisible(); + test('setDepartment - Called during ongoing conversation', async () => { + const [departmentA, departmentB] = departments.map(({ data }) => data); + const registerGuestVisitor = { + name: faker.person.firstName(), + email: faker.internet.email(), + token: faker.string.uuid(), + department: departmentA._id, + }; - await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); - await poLiveChat.btnSendMessageToOnlineAgent.click(); + // Start Chat + await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); + await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).toBeVisible(); - await test.step('Expect registered guest to be in dep1', async () => { - await poAuxContext.poHomeOmnichannel.sidenav.openChat(registerGuestVisitor.name); + await poLiveChat.page.evaluate( + (registerGuestVisitor) => window.RocketChat.livechat.registerGuest(registerGuestVisitor), + registerGuestVisitor, + ); + + await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).not.toBeVisible(); + + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + + await test.step('Expect registered guest to be in dep1', async () => { + await poAuxContext.poHomeOmnichannel.sidenav.openChat(registerGuestVisitor.name); + await expect(poAuxContext.poHomeOmnichannel.content.channelHeader).toContainText(registerGuestVisitor.name); + }); + + const depId = departmentB._id; + + await test.step('Expect chat not be transferred', async () => { + await poLiveChat.page.evaluate((depId) => window.RocketChat.livechat.setDepartment(depId), depId); + + await poAuxContext2.page.locator('role=navigation >> role=button[name=Search]').click(); + await poAuxContext2.page.locator('role=search >> role=searchbox').fill(registerGuestVisitor.name); + await expect( + poAuxContext2.page.locator(`role=search >> role=listbox >> role=link >> text="${registerGuestVisitor.name}"`), + ).not.toBeVisible(); + }); + + await test.step('Expect registered guest to still be in dep1', async () => { + await poAuxContext.poHomeOmnichannel.sidenav.openChat(registerGuestVisitor.name); + await expect(poAuxContext.poHomeOmnichannel.content.channelHeader).toContainText(registerGuestVisitor.name); + }); }); - const depId = departmentB._id; + test('setDepartment - Called before conversation', async () => { + const departmentB = departments[1].data; + const registerGuestVisitor = { + name: faker.person.firstName(), + email: faker.internet.email(), + }; + + const depId = departmentB._id; - await test.step('Expect setDepartment to change a guest department', async () => { await poLiveChat.page.evaluate((depId) => window.RocketChat.livechat.setDepartment(depId), depId); - }); - await test.step('Expect registered guest to be in dep2', async () => { - await poAuxContext2.poHomeOmnichannel.sidenav.openChat(registerGuestVisitor.name); + await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); + await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).toBeVisible(); + + await poLiveChat.sendMessage(registerGuestVisitor, false); + + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + + await test.step('Expect registered guest to be in dep2', async () => { + await poAuxContext2.page.locator('role=navigation >> role=button[name=Search]').click(); + await poAuxContext2.page.locator('role=search >> role=searchbox').fill(registerGuestVisitor.name); + await poAuxContext2.page.locator(`role=search >> role=listbox >> role=link >> text="${registerGuestVisitor.name}"`).click(); + await poAuxContext2.page.locator('role=main').waitFor(); + await poAuxContext2.page.locator('role=main >> role=heading[level=1]').waitFor(); + await expect(poAuxContext2.page.locator('role=main >> .rcx-skeleton')).toHaveCount(0); + await expect(poAuxContext2.page.locator('role=main >> role=list')).not.toHaveAttribute('aria-busy', 'true'); + }); + + await test.step('Expect registered guest not to be in dep1', async () => { + await poAuxContext.page.locator('role=navigation >> role=button[name=Search]').click(); + await poAuxContext.page.locator('role=search >> role=searchbox').fill(registerGuestVisitor.name); + await expect( + poAuxContext.page.locator(`role=search >> role=listbox >> role=link >> text="${registerGuestVisitor.name}"`), + ).not.toBeVisible(); + }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat-embedded.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat-embedded.ts index ec7015c4a11b7..79d1e9532677e 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat-embedded.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat-embedded.ts @@ -121,10 +121,10 @@ export class OmnichannelLiveChatEmbedded { public async sendMessage(liveChatUser: { name: string; email: string }, isOffline = true): Promise { const buttonLabel = isOffline ? 'Send' : 'Start chat'; - await this.inputName.type(liveChatUser.name); - await this.inputEmail.type(liveChatUser.email); + await this.inputName.fill(liveChatUser.name); + await this.inputEmail.fill(liveChatUser.email); if (isOffline) { - await this.textAreaMessage.type('any_message'); + await this.textAreaMessage.fill('any_message'); await this.btnSendMessage(buttonLabel).click(); return this.btnFinishOfflineMessage().click(); } diff --git a/packages/livechat/src/components/Form/CustomFields/index.tsx b/packages/livechat/src/components/Form/CustomFields/index.tsx index 8c037974e1155..3bca3375a9723 100644 --- a/packages/livechat/src/components/Form/CustomFields/index.tsx +++ b/packages/livechat/src/components/Form/CustomFields/index.tsx @@ -2,6 +2,7 @@ import type { Control, FieldErrors, FieldValues } from 'react-hook-form'; import { Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import type { RegisterFormValues } from '../../../routes/Register'; import { FormField } from '../FormField'; import { SelectInput } from '../SelectInput'; import { TextInput } from '../TextInput'; @@ -19,7 +20,7 @@ export type CustomField = { type RenderCustomFieldsProps = { customFields: CustomField[]; loading: boolean; - control: Control; + control: Control; errors: FieldErrors; }; diff --git a/packages/livechat/src/components/Form/SelectInput/index.tsx b/packages/livechat/src/components/Form/SelectInput/index.tsx index c785445b30211..3646b0147f3f5 100644 --- a/packages/livechat/src/components/Form/SelectInput/index.tsx +++ b/packages/livechat/src/components/Form/SelectInput/index.tsx @@ -1,6 +1,5 @@ import type { ComponentChild, Ref } from 'preact'; import type { TargetedEvent } from 'preact/compat'; -import { useState } from 'preact/hooks'; import type { JSXInternal } from 'preact/src/jsx'; import { createClassName } from '../../../helpers/createClassName'; @@ -38,38 +37,26 @@ export const SelectInput = ({ value, ref, }: SelectInputProps) => { - const [internalValue, setInternalValue] = useState(value); - const SelectOptions = Array.from(options).map(({ value, label }, key) => ( )); - const handleChange = (event: TargetedEvent) => { - onChange(event); - - if (event.defaultPrevented) { - return; - } - - setInternalValue((event.target as HTMLSelectElement)?.value); - }; - return (
setServerName(String(value))} /> + + ) => setRoomName(e.currentTarget.value)} + /> + + + {t('Manage_server_list')} + + + + + + ); +}; + +export default MatrixFederationSearchModalContent; diff --git a/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/index.ts b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/index.ts new file mode 100644 index 0000000000000..8447180665a03 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/index.ts @@ -0,0 +1 @@ +export { default } from './MatrixFederationSearch'; diff --git a/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/useInfiniteFederationSearchPublicRooms.ts b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/useInfiniteFederationSearchPublicRooms.ts new file mode 100644 index 0000000000000..6d80a7a9b3838 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/useInfiniteFederationSearchPublicRooms.ts @@ -0,0 +1,18 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useInfiniteQuery } from '@tanstack/react-query'; + +const tenMinutes = 10 * 60 * 1000; + +export const useInfiniteFederationSearchPublicRooms = (serverName: string, roomName?: string, count?: number) => { + const fetchRoomList = useEndpoint('GET', '/v1/federation/searchPublicRooms'); + return useInfiniteQuery( + ['federation/searchPublicRooms', serverName, roomName, count], + async ({ pageParam }) => fetchRoomList({ serverName, roomName, count, pageToken: pageParam }), + { + getNextPageParam: (lastPage) => lastPage.nextPageToken, + useErrorBoundary: true, + staleTime: tenMinutes, + cacheTime: tenMinutes, + }, + ); +}; diff --git a/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/useMatrixServerList.ts b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/useMatrixServerList.ts new file mode 100644 index 0000000000000..4f9ba64848f32 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/MatrixFederationSearch/useMatrixServerList.ts @@ -0,0 +1,10 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +export const useMatrixServerList = () => { + const fetchServerList = useEndpoint('GET', '/v1/federation/listServersByUser'); + return useQuery(['federation/listServersByUsers'], async () => fetchServerList(), { + useErrorBoundary: true, + staleTime: Infinity, + }); +}; diff --git a/apps/meteor/client/sidebarv2/header/SearchList.tsx b/apps/meteor/client/sidebarv2/header/SearchList.tsx new file mode 100644 index 0000000000000..70c666fead502 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/SearchList.tsx @@ -0,0 +1,79 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useTranslation, useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; +import type { MouseEventHandler, ReactElement } from 'react'; +import React, { useMemo, useRef } from 'react'; +import { Virtuoso } from 'react-virtuoso'; + +import { VirtuosoScrollbars } from '../../components/CustomScrollbars'; +import RoomListWrapper from '../RoomList/RoomListWrapper'; +import { useAvatarTemplate } from '../hooks/useAvatarTemplate'; +import { usePreventDefault } from '../hooks/usePreventDefault'; +import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; +import Row from '../search/Row'; +import { useSearchItems } from './hooks/useSearchItems'; + +type SearchListProps = { filterText: string; onEscSearch: () => void }; + +const SearchList = ({ filterText, onEscSearch }: SearchListProps) => { + const t = useTranslation(); + + const boxRef = useRef(null); + usePreventDefault(boxRef); + + const { data: items = [], isLoading } = useSearchItems(filterText); + + const sidebarViewMode = useUserPreference('sidebarViewMode'); + const useRealName = useSetting('UI_Use_Real_Name'); + + const sideBarItemTemplate = useTemplateByViewMode(); + const avatarTemplate = useAvatarTemplate(); + + const extended = sidebarViewMode === 'extended'; + + const itemData = useMemo( + () => ({ + items, + t, + SideBarItemTemplate: sideBarItemTemplate, + avatarTemplate, + useRealName, + extended, + sidebarViewMode, + }), + [avatarTemplate, extended, items, useRealName, sideBarItemTemplate, sidebarViewMode, t], + ); + + const handleClick: MouseEventHandler = (e): void => { + if (e.target instanceof Element && [e.target.tagName, e.target.parentElement?.tagName].includes('BUTTON')) { + return; + } + return onEscSearch(); + }; + + return ( + + room._id} + itemContent={(_, data): ReactElement => } + /> + + ); +}; + +export default SearchList; diff --git a/apps/meteor/client/sidebarv2/header/SearchSection.tsx b/apps/meteor/client/sidebarv2/header/SearchSection.tsx new file mode 100644 index 0000000000000..66d8eb65371fe --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/SearchSection.tsx @@ -0,0 +1,104 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Icon, TextInput, Palette, Sidebar } from '@rocket.chat/fuselage'; +import { useMergedRefs, useOutsideClick } from '@rocket.chat/fuselage-hooks'; +import { useTranslation, useUser } from '@rocket.chat/ui-contexts'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { useForm } from 'react-hook-form'; +import tinykeys from 'tinykeys'; + +import SearchList from './SearchList'; +import CreateRoom from './actions/CreateRoom'; +import Sort from './actions/Sort'; + +const wrapperStyle = css` + position: absolute; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + z-index: 99; + top: 0; + left: 0; + background-color: ${Palette.surface['surface-sidebar']}; +`; + +const SearchSection = () => { + const t = useTranslation(); + const user = useUser(); + + const { + formState: { isDirty }, + register, + watch, + resetField, + setFocus, + } = useForm({ defaultValues: { filterText: '' } }); + const { filterText } = watch(); + const { ref: filterRef, ...rest } = register('filterText'); + + const inputRef = useRef(null); + const wrapperRef = useRef(null); + const mergedRefs = useMergedRefs(filterRef, inputRef); + + const handleEscSearch = useCallback(() => { + resetField('filterText'); + inputRef.current?.blur(); + }, [resetField]); + + useOutsideClick([wrapperRef], handleEscSearch); + + useEffect(() => { + const unsubscribe = tinykeys(window, { + '$mod+K': (event) => { + event.preventDefault(); + setFocus('filterText'); + }, + '$mod+P': (event) => { + event.preventDefault(); + setFocus('filterText'); + }, + 'Escape': (event) => { + event.preventDefault(); + handleEscSearch(); + }, + }); + + return (): void => { + unsubscribe(); + }; + }, [handleEscSearch, setFocus]); + + return ( + + + } + /> + + {user && !isDirty && ( + <> + + + + )} + + + {isDirty && } + + ); +}; + +export default SearchSection; diff --git a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx b/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx new file mode 100644 index 0000000000000..478e7cce33e1b --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx @@ -0,0 +1,19 @@ +import { Sidebar } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { HTMLAttributes } from 'react'; +import React from 'react'; + +import GenericMenu from '../../../components/GenericMenu/GenericMenu'; +import { useCreateRoom } from './hooks/useCreateRoomMenu'; + +type CreateRoomProps = Omit, 'is'>; + +const CreateRoom = (props: CreateRoomProps) => { + const t = useTranslation(); + + const sections = useCreateRoom(); + + return ; +}; + +export default CreateRoom; diff --git a/apps/meteor/client/sidebarv2/header/actions/Search.tsx b/apps/meteor/client/sidebarv2/header/actions/Search.tsx new file mode 100644 index 0000000000000..06d42114d76b8 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/Search.tsx @@ -0,0 +1,50 @@ +import { Sidebar } from '@rocket.chat/fuselage'; +import { useEffectEvent, useOutsideClick } from '@rocket.chat/fuselage-hooks'; +import type { HTMLAttributes } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; +import tinykeys from 'tinykeys'; + +import SearchList from '../../search/SearchList'; + +type SearchProps = Omit, 'is'>; + +const Search = (props: SearchProps) => { + const [searchOpen, setSearchOpen] = useState(false); + + const ref = useRef(null); + const handleCloseSearch = useEffectEvent(() => { + setSearchOpen(false); + }); + + useOutsideClick([ref], handleCloseSearch); + + const openSearch = useEffectEvent(() => { + setSearchOpen(true); + }); + + useEffect(() => { + const unsubscribe = tinykeys(window, { + '$mod+K': (event) => { + event.preventDefault(); + openSearch(); + }, + '$mod+P': (event) => { + event.preventDefault(); + openSearch(); + }, + }); + + return (): void => { + unsubscribe(); + }; + }, [openSearch]); + + return ( + <> + + {searchOpen && } + + ); +}; + +export default Search; diff --git a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx b/apps/meteor/client/sidebarv2/header/actions/Sort.tsx new file mode 100644 index 0000000000000..e7f3b398e5f62 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/Sort.tsx @@ -0,0 +1,21 @@ +import { Sidebar } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { HTMLAttributes } from 'react'; +import React from 'react'; + +import GenericMenu from '../../../components/GenericMenu/GenericMenu'; +import { useSortMenu } from './hooks/useSortMenu'; + +type SortProps = Omit, 'is'>; + +const Sort = (props: SortProps) => { + const t = useTranslation(); + + const sections = useSortMenu(); + + return ( + + ); +}; + +export default Sort; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx new file mode 100644 index 0000000000000..3935ad0039df6 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx @@ -0,0 +1,68 @@ +import { useTranslation, useSetting, useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; + +import CreateDiscussion from '../../../../components/CreateDiscussion'; +import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; +import CreateChannelModal from '../../CreateChannelModal'; +import CreateDirectMessage from '../../CreateDirectMessage'; +import CreateTeamModal from '../../CreateTeamModal'; +import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; + +const CREATE_CHANNEL_PERMISSIONS = ['create-c', 'create-p']; +const CREATE_TEAM_PERMISSIONS = ['create-team']; +const CREATE_DIRECT_PERMISSIONS = ['create-d']; +const CREATE_DISCUSSION_PERMISSIONS = ['start-discussion', 'start-discussion-other-user']; + +export const useCreateRoomItems = (): GenericMenuItemProps[] => { + const t = useTranslation(); + const discussionEnabled = useSetting('Discussion_enabled'); + + const canCreateChannel = useAtLeastOnePermission(CREATE_CHANNEL_PERMISSIONS); + const canCreateTeam = useAtLeastOnePermission(CREATE_TEAM_PERMISSIONS); + const canCreateDirectMessages = useAtLeastOnePermission(CREATE_DIRECT_PERMISSIONS); + const canCreateDiscussion = useAtLeastOnePermission(CREATE_DISCUSSION_PERMISSIONS); + + const createChannel = useCreateRoomModal(CreateChannelModal); + const createTeam = useCreateRoomModal(CreateTeamModal); + const createDiscussion = useCreateRoomModal(CreateDiscussion); + const createDirectMessage = useCreateRoomModal(CreateDirectMessage); + + const createChannelItem: GenericMenuItemProps = { + id: 'channel', + content: t('Channel'), + icon: 'hashtag', + onClick: () => { + createChannel(); + }, + }; + const createTeamItem: GenericMenuItemProps = { + id: 'team', + content: t('Team'), + icon: 'team', + onClick: () => { + createTeam(); + }, + }; + const createDirectMessageItem: GenericMenuItemProps = { + id: 'direct', + content: t('Direct_message'), + icon: 'balloon', + onClick: () => { + createDirectMessage(); + }, + }; + const createDiscussionItem: GenericMenuItemProps = { + id: 'discussion', + content: t('Discussion'), + icon: 'discussion', + onClick: () => { + createDiscussion(); + }, + }; + + return [ + ...(canCreateDirectMessages ? [createDirectMessageItem] : []), + ...(canCreateDiscussion && discussionEnabled ? [createDiscussionItem] : []), + ...(canCreateChannel ? [createChannelItem] : []), + ...(canCreateTeam ? [createTeamItem] : []), + ]; +}; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx new file mode 100644 index 0000000000000..6a0c58b36311e --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomMenu.tsx @@ -0,0 +1,25 @@ +import { useAtLeastOnePermission, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; + +import { useIsEnterprise } from '../../../../hooks/useIsEnterprise'; +import { useCreateRoomItems } from './useCreateRoomItems'; +import { useMatrixFederationItems } from './useMatrixFederationItems'; + +const CREATE_ROOM_PERMISSIONS = ['create-c', 'create-p', 'create-d', 'start-discussion', 'start-discussion-other-user']; + +export const useCreateRoom = () => { + const t = useTranslation(); + const showCreate = useAtLeastOnePermission(CREATE_ROOM_PERMISSIONS); + + const { data } = useIsEnterprise(); + const isMatrixEnabled = useSetting('Federation_Matrix_enabled') && data?.isEnterprise; + + const createRoomItems = useCreateRoomItems(); + const matrixFederationSearchItems = useMatrixFederationItems({ isMatrixEnabled }); + + const sections = [ + { title: t('Create_new'), items: createRoomItems, permission: showCreate }, + { title: t('Explore'), items: matrixFederationSearchItems, permission: showCreate && isMatrixEnabled }, + ]; + + return sections.filter((section) => section.permission); +}; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.spec.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.spec.tsx new file mode 100644 index 0000000000000..b5779d8252024 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.spec.tsx @@ -0,0 +1,25 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useGroupingListItems } from './useGroupingListItems'; + +it('should render groupingList items', async () => { + const { result } = renderHook(() => useGroupingListItems()); + + expect(result.current[0]).toEqual( + expect.objectContaining({ + id: 'unread', + }), + ); + + expect(result.current[1]).toEqual( + expect.objectContaining({ + id: 'favorites', + }), + ); + + expect(result.current[2]).toEqual( + expect.objectContaining({ + id: 'types', + }), + ); +}); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx new file mode 100644 index 0000000000000..646b85c838be4 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx @@ -0,0 +1,43 @@ +import { CheckBox } from '@rocket.chat/fuselage'; +import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; + +export const useGroupingListItems = (): GenericMenuItemProps[] => { + const t = useTranslation(); + + const sidebarGroupByType = useUserPreference('sidebarGroupByType'); + const sidebarShowFavorites = useUserPreference('sidebarShowFavorites'); + const sidebarShowUnread = useUserPreference('sidebarShowUnread'); + + const saveUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); + + const useHandleChange = (key: 'sidebarGroupByType' | 'sidebarShowFavorites' | 'sidebarShowUnread', value: boolean): (() => void) => + useCallback(() => saveUserPreferences({ data: { [key]: value } }), [key, value]); + + const handleChangeGroupByType = useHandleChange('sidebarGroupByType', !sidebarGroupByType); + const handleChangeShoFavorite = useHandleChange('sidebarShowFavorites', !sidebarShowFavorites); + const handleChangeShowUnread = useHandleChange('sidebarShowUnread', !sidebarShowUnread); + + return [ + { + id: 'unread', + content: t('Unread'), + icon: 'flag', + addon: , + }, + { + id: 'favorites', + content: t('Favorites'), + icon: 'star', + addon: , + }, + { + id: 'types', + content: t('Types'), + icon: 'group-by-type', + addon: , + }, + ]; +}; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts b/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts new file mode 100644 index 0000000000000..b3ac63f137738 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts @@ -0,0 +1,26 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; + +import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; +import MatrixFederationSearch from '../../MatrixFederationSearch'; +import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; + +export const useMatrixFederationItems = ({ + isMatrixEnabled, +}: { + isMatrixEnabled: string | number | boolean | null | undefined; +}): GenericMenuItemProps[] => { + const t = useTranslation(); + + const searchFederatedRooms = useCreateRoomModal(MatrixFederationSearch); + + const matrixFederationSearchItem: GenericMenuItemProps = { + id: 'matrix-federation-search', + content: t('Federation_Search_federated_rooms'), + icon: 'magnifier', + onClick: () => { + searchFederatedRooms(); + }, + }; + + return [...(isMatrixEnabled ? [matrixFederationSearchItem] : [])]; +}; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortMenu.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortMenu.tsx new file mode 100644 index 0000000000000..bea1d999997e9 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortMenu.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; + +import { useGroupingListItems } from './useGroupingListItems'; +import { useSortModeItems } from './useSortModeItems'; +import { useViewModeItems } from './useViewModeItems'; + +export const useSortMenu = () => { + const t = useTranslation(); + + const viewModeItems = useViewModeItems(); + const sortModeItems = useSortModeItems(); + const groupingListItems = useGroupingListItems(); + + const sections = [ + { title: t('Display'), items: viewModeItems }, + { title: t('Sort_By'), items: sortModeItems }, + { title: t('Group_by'), items: groupingListItems }, + ]; + + return sections; +}; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.spec.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.spec.tsx new file mode 100644 index 0000000000000..143d228fe7ca9 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.spec.tsx @@ -0,0 +1,19 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useSortModeItems } from './useSortModeItems'; + +it('should render sortMode items', async () => { + const { result } = renderHook(() => useSortModeItems()); + + expect(result.current[0]).toEqual( + expect.objectContaining({ + id: 'activity', + }), + ); + + expect(result.current[1]).toEqual( + expect.objectContaining({ + id: 'name', + }), + ); +}); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx new file mode 100644 index 0000000000000..56041ab4e571c --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx @@ -0,0 +1,40 @@ +import { RadioButton } from '@rocket.chat/fuselage'; +import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; +import { + OmnichannelSortingDisclaimer, + useOmnichannelSortingDisclaimer, +} from '../../../../components/Omnichannel/OmnichannelSortingDisclaimer'; + +export const useSortModeItems = (): GenericMenuItemProps[] => { + const t = useTranslation(); + + const saveUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); + const sidebarSortBy = useUserPreference<'activity' | 'alphabetical'>('sidebarSortby', 'activity'); + const isOmnichannelEnabled = useOmnichannelSortingDisclaimer(); + + const useHandleChange = (value: 'alphabetical' | 'activity'): (() => void) => + useCallback(() => saveUserPreferences({ data: { sidebarSortby: value } }), [value]); + + const setToAlphabetical = useHandleChange('alphabetical'); + const setToActivity = useHandleChange('activity'); + + return [ + { + id: 'activity', + content: t('Activity'), + icon: 'clock', + addon: , + description: sidebarSortBy === 'activity' && isOmnichannelEnabled && , + }, + { + id: 'name', + content: t('Name'), + icon: 'sort-az', + addon: , + description: sidebarSortBy === 'alphabetical' && isOmnichannelEnabled && , + }, + ]; +}; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.spec.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.spec.tsx new file mode 100644 index 0000000000000..6c6dd7532e7e8 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.spec.tsx @@ -0,0 +1,31 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useViewModeItems } from './useViewModeItems'; + +it('should render viewMode items', async () => { + const { result } = renderHook(() => useViewModeItems()); + + expect(result.current[0]).toEqual( + expect.objectContaining({ + id: 'extended', + }), + ); + + expect(result.current[1]).toEqual( + expect.objectContaining({ + id: 'medium', + }), + ); + + expect(result.current[2]).toEqual( + expect.objectContaining({ + id: 'condensed', + }), + ); + + expect(result.current[3]).toEqual( + expect.objectContaining({ + id: 'avatars', + }), + ); +}); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx new file mode 100644 index 0000000000000..ca2855d09db59 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx @@ -0,0 +1,53 @@ +import { RadioButton, ToggleSwitch } from '@rocket.chat/fuselage'; +import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; + +export const useViewModeItems = (): GenericMenuItemProps[] => { + const t = useTranslation(); + + const saveUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); + + const useHandleChange = (value: 'medium' | 'extended' | 'condensed'): (() => void) => + useCallback(() => saveUserPreferences({ data: { sidebarViewMode: value } }), [value]); + + const sidebarViewMode = useUserPreference<'medium' | 'extended' | 'condensed'>('sidebarViewMode', 'extended'); + const sidebarDisplayAvatar = useUserPreference('sidebarDisplayAvatar', false); + + const setToExtended = useHandleChange('extended'); + const setToMedium = useHandleChange('medium'); + const setToCondensed = useHandleChange('condensed'); + + const handleChangeSidebarDisplayAvatar = useCallback( + () => saveUserPreferences({ data: { sidebarDisplayAvatar: !sidebarDisplayAvatar } }), + [saveUserPreferences, sidebarDisplayAvatar], + ); + + return [ + { + id: 'extended', + content: t('Extended'), + icon: 'extended-view', + addon: , + }, + { + id: 'medium', + content: t('Medium'), + icon: 'medium-view', + addon: , + }, + { + id: 'condensed', + content: t('Condensed'), + icon: 'condensed-view', + addon: , + }, + { + id: 'avatars', + content: t('Avatars'), + icon: 'user-rounded', + addon: , + }, + ]; +}; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useCreateRoomModal.tsx b/apps/meteor/client/sidebarv2/header/hooks/useCreateRoomModal.tsx new file mode 100644 index 0000000000000..70e14f80adf65 --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/hooks/useCreateRoomModal.tsx @@ -0,0 +1,16 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import type { FC } from 'react'; +import React from 'react'; + +export const useCreateRoomModal = (Component: FC): (() => void) => { + const setModal = useSetModal(); + + return useEffectEvent(() => { + const handleClose = (): void => { + setModal(null); + }; + + setModal(() => ); + }); +}; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useDropdownVisibility.ts b/apps/meteor/client/sidebarv2/header/hooks/useDropdownVisibility.ts new file mode 100644 index 0000000000000..390486d1727de --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/hooks/useDropdownVisibility.ts @@ -0,0 +1,38 @@ +import { useToggle, useOutsideClick } from '@rocket.chat/fuselage-hooks'; +import type { RefObject } from 'react'; +import { useCallback } from 'react'; + +/** + * useDropdownVisibility + * is used to control the visibility of a dropdown + * also checks if the user clicked outside the dropdown, but ignores if the click was on the anchor + * @param {Object} props + * @param {Object} props.reference - The reference where the dropdown will be attached to + * @param {Object} props.target - The target, the dropdown itself + * @returns {Object} + * @returns {Boolean} isVisible - The visibility of the dropdown + * @returns {Function} toggle - The function to toggle the dropdown + */ + +export const useDropdownVisibility = ({ + reference, + target, +}: { + reference: RefObject; + target: RefObject; +}): { + isVisible: boolean; + toggle: (state?: boolean) => void; +} => { + const [isVisible, toggle] = useToggle(false); + + useOutsideClick( + [target, reference], + useCallback(() => toggle(false), [toggle]), + ); + + return { + isVisible, + toggle, + }; +}; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx b/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx new file mode 100644 index 0000000000000..09796dd7a6b7e --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/hooks/useEncryptedRoomDescription.tsx @@ -0,0 +1,23 @@ +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; + +export const useEncryptedRoomDescription = (roomType: 'channel' | 'team') => { + const t = useTranslation(); + const e2eEnabled = useSetting('E2E_Enable'); + const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); + + return ({ isPrivate, broadcast, encrypted }: { isPrivate: boolean; broadcast: boolean; encrypted: boolean }) => { + if (!e2eEnabled) { + return t('Not_available_for_this_workspace'); + } + if (!isPrivate) { + return t('Encrypted_not_available', { roomType }); + } + if (broadcast) { + return t('Not_available_for_broadcast', { roomType }); + } + if (e2eEnabledForPrivateByDefault || encrypted) { + return t('Encrypted_messages', { roomType }); + } + return t('Encrypted_messages_false'); + }; +}; diff --git a/apps/meteor/client/sidebarv2/header/hooks/useSearchItems.ts b/apps/meteor/client/sidebarv2/header/hooks/useSearchItems.ts new file mode 100644 index 0000000000000..7dfdb577dd9fd --- /dev/null +++ b/apps/meteor/client/sidebarv2/header/hooks/useSearchItems.ts @@ -0,0 +1,114 @@ +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { useMethod, useUserSubscriptions } from '@rocket.chat/ui-contexts'; +import { useQuery, type UseQueryResult } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import { getConfig } from '../../../lib/utils/getConfig'; + +const LIMIT = parseInt(String(getConfig('Sidebar_Search_Spotlight_LIMIT', 20))); + +const options = { + sort: { + lm: -1, + name: 1, + }, + limit: LIMIT, +} as const; + +export const useSearchItems = (filterText: string): UseQueryResult<(ISubscription & IRoom)[] | undefined, Error> => { + const [, mention, name] = useMemo(() => filterText.match(/(@|#)?(.*)/i) || [], [filterText]); + const query = useMemo(() => { + const filterRegex = new RegExp(escapeRegExp(name), 'i'); + + return { + $or: [{ name: filterRegex }, { fname: filterRegex }], + ...(mention && { + t: mention === '@' ? 'd' : { $ne: 'd' }, + }), + }; + }, [name, mention]); + + const localRooms = useUserSubscriptions(query, options); + + const usernamesFromClient = [...localRooms?.map(({ t, name }) => (t === 'd' ? name : null))].filter(Boolean) as string[]; + + const searchForChannels = mention === '#'; + const searchForDMs = mention === '@'; + + const type = useMemo(() => { + if (searchForChannels) { + return { users: false, rooms: true, includeFederatedRooms: true }; + } + if (searchForDMs) { + return { users: true, rooms: false }; + } + return { users: true, rooms: true, includeFederatedRooms: true }; + }, [searchForChannels, searchForDMs]); + + const getSpotlight = useMethod('spotlight'); + + return useQuery( + ['sidebar/search/spotlight', name, usernamesFromClient, type, localRooms.map(({ _id, name }) => _id + name)], + async () => { + if (localRooms.length === LIMIT) { + return localRooms; + } + + const spotlight = await getSpotlight(name, usernamesFromClient, type); + + const filterUsersUnique = ({ _id }: { _id: string }, index: number, arr: { _id: string }[]): boolean => + index === arr.findIndex((user) => _id === user._id); + + const roomFilter = (room: { t: string; uids?: string[]; _id: string; name?: string }): boolean => + !localRooms.find( + (item) => + (room.t === 'd' && room.uids && room.uids.length > 1 && room.uids?.includes(item._id)) || + [item.rid, item._id].includes(room._id), + ); + const usersFilter = (user: { _id: string }): boolean => + !localRooms.find((room) => room.t === 'd' && room.uids && room.uids?.length === 2 && room.uids.includes(user._id)); + + const userMap = (user: { + _id: string; + name: string; + username: string; + avatarETag?: string; + }): { + _id: string; + t: string; + name: string; + fname: string; + avatarETag?: string; + } => ({ + _id: user._id, + t: 'd', + name: user.username, + fname: user.name, + avatarETag: user.avatarETag, + }); + + type resultsFromServerType = { + _id: string; + t: string; + name: string; + teamMain?: boolean; + fname?: string; + avatarETag?: string | undefined; + uids?: string[] | undefined; + }[]; + + const resultsFromServer: resultsFromServerType = []; + resultsFromServer.push(...spotlight.users.filter(filterUsersUnique).filter(usersFilter).map(userMap)); + resultsFromServer.push(...spotlight.rooms.filter(roomFilter)); + + const exact = resultsFromServer?.filter((item) => [item.name, item.fname].includes(name)); + return Array.from(new Set([...exact, ...localRooms, ...resultsFromServer])); + }, + { + staleTime: 60_000, + keepPreviousData: true, + placeholderData: localRooms, + }, + ); +}; diff --git a/apps/meteor/client/sidebarv2/hooks/useAvatarTemplate.tsx b/apps/meteor/client/sidebarv2/hooks/useAvatarTemplate.tsx new file mode 100644 index 0000000000000..9fd1023a32e72 --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useAvatarTemplate.tsx @@ -0,0 +1,39 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { RoomAvatar } from '@rocket.chat/ui-avatar'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ComponentType } from 'react'; +import React, { useMemo } from 'react'; + +export const useAvatarTemplate = ( + sidebarViewMode?: 'extended' | 'medium' | 'condensed', + sidebarDisplayAvatar?: boolean, +): null | ComponentType => { + const sidebarViewModeFromSettings = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode'); + const sidebarDisplayAvatarFromSettings = useUserPreference('sidebarDisplayAvatar'); + + const viewMode = sidebarViewMode ?? sidebarViewModeFromSettings; + const displayAvatar = sidebarDisplayAvatar ?? sidebarDisplayAvatarFromSettings; + return useMemo(() => { + if (!displayAvatar) { + return null; + } + + const size = ((): 'x36' | 'x28' | 'x16' => { + switch (viewMode) { + case 'extended': + return 'x36'; + case 'medium': + return 'x28'; + case 'condensed': + default: + return 'x16'; + } + })(); + + const renderRoomAvatar: ComponentType = (room) => ( + + ); + + return renderRoomAvatar; + }, [displayAvatar, viewMode]); +}; diff --git a/apps/meteor/client/sidebarv2/hooks/usePreventDefault.ts b/apps/meteor/client/sidebarv2/hooks/usePreventDefault.ts new file mode 100644 index 0000000000000..9d3ca18da35e2 --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/usePreventDefault.ts @@ -0,0 +1,21 @@ +import type { RefObject } from 'react'; +import { useEffect } from 'react'; + +export const usePreventDefault = (ref: RefObject): { ref: RefObject } => { + // Flowrouter uses an addEventListener on the document to capture any clink link, since the react synthetic event use an addEventListener on the document too, + // it is impossible/hard to determine which one will happen before and prevent/stop propagation, so feel free to remove this effect after remove flow router :) + + useEffect(() => { + const { current } = ref; + const stopPropagation: EventListener = (e) => { + if ([(e.target as HTMLElement).nodeName, (e.target as HTMLElement).parentElement?.nodeName].includes('BUTTON')) { + e.preventDefault(); + } + }; + current?.addEventListener('click', stopPropagation); + + return (): void => current?.addEventListener('click', stopPropagation); + }, [ref]); + + return { ref }; +}; diff --git a/apps/meteor/client/sidebarv2/hooks/useQueryOptions.ts b/apps/meteor/client/sidebarv2/hooks/useQueryOptions.ts new file mode 100644 index 0000000000000..55fd137759d11 --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useQueryOptions.ts @@ -0,0 +1,32 @@ +import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +export const useQueryOptions = (): { + sort: + | { + lm?: -1 | 1 | undefined; + } + | { + lowerCaseFName: -1 | 1; + lm?: -1 | 1 | undefined; + } + | { + lowerCaseName: -1 | 1; + lm?: -1 | 1 | undefined; + }; +} => { + const sortBy = useUserPreference('sidebarSortby'); + const showRealName = useSetting('UI_Use_Real_Name'); + + return useMemo( + () => ({ + sort: { + ...(sortBy === 'activity' && { lm: -1 }), + ...(sortBy !== 'activity' && { + ...(showRealName ? { lowerCaseFName: 1 } : { lowerCaseName: 1 }), + }), + }, + }), + [sortBy, showRealName], + ); +}; diff --git a/apps/meteor/client/sidebarv2/hooks/useRoomList.ts b/apps/meteor/client/sidebarv2/hooks/useRoomList.ts new file mode 100644 index 0000000000000..fa5dfd2797cb1 --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useRoomList.ts @@ -0,0 +1,122 @@ +import type { ILivechatInquiryRecord, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { useDebouncedState } from '@rocket.chat/fuselage-hooks'; +import { useUserPreference, useUserSubscriptions, useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { useVideoConfIncomingCalls } from '../../contexts/VideoConfContext'; +import { useOmnichannelEnabled } from '../../hooks/omnichannel/useOmnichannelEnabled'; +import { useQueuedInquiries } from '../../hooks/omnichannel/useQueuedInquiries'; +import { useQueryOptions } from './useQueryOptions'; + +const query = { open: { $ne: false } }; + +const emptyQueue: ILivechatInquiryRecord[] = []; + +export const useRoomList = (): Array => { + const [roomList, setRoomList] = useDebouncedState<(ISubscription & IRoom)[]>([], 150); + + const showOmnichannel = useOmnichannelEnabled(); + const sidebarGroupByType = useUserPreference('sidebarGroupByType'); + const favoritesEnabled = useUserPreference('sidebarShowFavorites'); + const isDiscussionEnabled = useSetting('Discussion_enabled'); + const sidebarShowUnread = useUserPreference('sidebarShowUnread'); + + const options = useQueryOptions(); + + const rooms = useUserSubscriptions(query, options); + + const inquiries = useQueuedInquiries(); + + const incomingCalls = useVideoConfIncomingCalls(); + + let queue = emptyQueue; + if (inquiries.enabled) { + queue = inquiries.queue; + } + + useEffect(() => { + setRoomList(() => { + const incomingCall = new Set(); + const favorite = new Set(); + const team = new Set(); + const omnichannel = new Set(); + const unread = new Set(); + const channels = new Set(); + const direct = new Set(); + const discussion = new Set(); + const conversation = new Set(); + const onHold = new Set(); + + rooms.forEach((room) => { + if (room.archived) { + return; + } + + if (incomingCalls.find((call) => call.rid === room.rid)) { + return incomingCall.add(room); + } + + if (sidebarShowUnread && (room.alert || room.unread) && !room.hideUnreadStatus) { + return unread.add(room); + } + + if (favoritesEnabled && room.f) { + return favorite.add(room); + } + + if (sidebarGroupByType && room.teamMain) { + return team.add(room); + } + + if (sidebarGroupByType && isDiscussionEnabled && room.prid) { + return discussion.add(room); + } + + if (room.t === 'c' || room.t === 'p') { + channels.add(room); + } + + if (room.t === 'l' && room.onHold) { + return showOmnichannel && onHold.add(room); + } + + if (room.t === 'l') { + return showOmnichannel && omnichannel.add(room); + } + + if (room.t === 'd') { + direct.add(room); + } + + conversation.add(room); + }); + + const groups = new Map(); + incomingCall.size && groups.set('Incoming Calls', incomingCall); + showOmnichannel && inquiries.enabled && queue.length && groups.set('Incoming_Livechats', queue); + showOmnichannel && omnichannel.size && groups.set('Open_Livechats', omnichannel); + showOmnichannel && onHold.size && groups.set('On_Hold_Chats', onHold); + sidebarShowUnread && unread.size && groups.set('Unread', unread); + favoritesEnabled && favorite.size && groups.set('Favorites', favorite); + sidebarGroupByType && team.size && groups.set('Teams', team); + sidebarGroupByType && isDiscussionEnabled && discussion.size && groups.set('Discussions', discussion); + sidebarGroupByType && channels.size && groups.set('Channels', channels); + sidebarGroupByType && direct.size && groups.set('Direct_Messages', direct); + !sidebarGroupByType && groups.set('Conversations', conversation); + return [...groups.entries()].flatMap(([key, group]) => [key, ...group]); + }); + }, [ + rooms, + showOmnichannel, + incomingCalls, + inquiries.enabled, + queue, + sidebarShowUnread, + favoritesEnabled, + sidebarGroupByType, + setRoomList, + isDiscussionEnabled, + ]); + + return roomList; +}; diff --git a/apps/meteor/client/sidebarv2/hooks/useShortcutOpenMenu.ts b/apps/meteor/client/sidebarv2/hooks/useShortcutOpenMenu.ts new file mode 100644 index 0000000000000..9898e67040e13 --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useShortcutOpenMenu.ts @@ -0,0 +1,21 @@ +import type { RefObject } from 'react'; +import { useEffect } from 'react'; +import tinykeys from 'tinykeys'; + +// used to open the menu option by keyboard +export const useShortcutOpenMenu = (ref: RefObject): void => { + useEffect(() => { + const unsubscribe = tinykeys(ref.current as HTMLElement, { + Alt: (event) => { + if (!(event.target as HTMLElement).className.includes('rcx-sidebar-item')) { + return; + } + event.preventDefault(); + (event.target as HTMLElement).querySelector('button')?.click(); + }, + }); + return (): void => { + unsubscribe(); + }; + }, [ref]); +}; diff --git a/apps/meteor/client/sidebarv2/hooks/useTemplateByViewMode.ts b/apps/meteor/client/sidebarv2/hooks/useTemplateByViewMode.ts new file mode 100644 index 0000000000000..2362669f3ebdf --- /dev/null +++ b/apps/meteor/client/sidebarv2/hooks/useTemplateByViewMode.ts @@ -0,0 +1,22 @@ +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ComponentType } from 'react'; +import { useMemo } from 'react'; + +import Condensed from '../Item/Condensed'; +import Extended from '../Item/Extended'; +import Medium from '../Item/Medium'; + +export const useTemplateByViewMode = (): ComponentType => { + const sidebarViewMode = useUserPreference('sidebarViewMode'); + return useMemo(() => { + switch (sidebarViewMode) { + case 'extended': + return Extended; + case 'medium': + return Medium; + case 'condensed': + default: + return Condensed; + } + }, [sidebarViewMode]); +}; diff --git a/apps/meteor/client/sidebarv2/index.ts b/apps/meteor/client/sidebarv2/index.ts new file mode 100644 index 0000000000000..55cd4f79dbf85 --- /dev/null +++ b/apps/meteor/client/sidebarv2/index.ts @@ -0,0 +1 @@ +export { default } from './SidebarRegion'; diff --git a/apps/meteor/client/sidebarv2/search/Row.tsx b/apps/meteor/client/sidebarv2/search/Row.tsx new file mode 100644 index 0000000000000..68ceecd2ad887 --- /dev/null +++ b/apps/meteor/client/sidebarv2/search/Row.tsx @@ -0,0 +1,40 @@ +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { ReactElement } from 'react'; +import React, { memo } from 'react'; + +import SideBarItemTemplateWithData from '../RoomList/SideBarItemTemplateWithData'; +import UserItem from './UserItem'; + +type RowProps = { + item: ISubscription & IRoom; + data: Record; +}; + +const Row = ({ item, data }: RowProps): ReactElement => { + const { t, SideBarItemTemplate, avatarTemplate: AvatarTemplate, useRealName, extended } = data; + + if (item.t === 'd' && !item.u) { + return ( + + ); + } + return ( + + ); +}; + +export default memo(Row); diff --git a/apps/meteor/client/sidebarv2/search/SearchList.tsx b/apps/meteor/client/sidebarv2/search/SearchList.tsx new file mode 100644 index 0000000000000..c43fe854ac309 --- /dev/null +++ b/apps/meteor/client/sidebarv2/search/SearchList.tsx @@ -0,0 +1,382 @@ +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { css } from '@rocket.chat/css-in-js'; +import { Sidebar, TextInput, Box, Icon } from '@rocket.chat/fuselage'; +import { useMutableCallback, useDebouncedValue, useAutoFocus, useUniqueId, useMergedRefs } from '@rocket.chat/fuselage-hooks'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { useUserPreference, useUserSubscriptions, useSetting, useTranslation, useMethod } from '@rocket.chat/ui-contexts'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import type { + ReactElement, + MutableRefObject, + SetStateAction, + Dispatch, + FormEventHandler, + Ref, + MouseEventHandler, + ForwardedRef, +} from 'react'; +import React, { forwardRef, useState, useMemo, useEffect, useRef } from 'react'; +import type { VirtuosoHandle } from 'react-virtuoso'; +import { Virtuoso } from 'react-virtuoso'; +import tinykeys from 'tinykeys'; + +import { VirtuosoScrollbars } from '../../components/CustomScrollbars'; +import { getConfig } from '../../lib/utils/getConfig'; +import { useAvatarTemplate } from '../hooks/useAvatarTemplate'; +import { usePreventDefault } from '../hooks/usePreventDefault'; +import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode'; +import Row from './Row'; + +const mobileCheck = function () { + let check = false; + (function (a: string) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + a, + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4), + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera || ''); + return check; +}; + +declare global { + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Window { + opera?: string; + } + // eslint-disable-next-line @typescript-eslint/naming-convention + interface Navigator { + userAgentData?: { + mobile: boolean; + }; + } +} + +const shortcut = ((): string => { + if (navigator.userAgentData?.mobile || mobileCheck()) { + return ''; + } + if (window.navigator.platform.toLowerCase().includes('mac')) { + return '(\u2318+K)'; + } + return '(Ctrl+K)'; +})(); + +const LIMIT = parseInt(String(getConfig('Sidebar_Search_Spotlight_LIMIT', 20))); + +const options = { + sort: { + lm: -1, + name: 1, + }, + limit: LIMIT, +} as const; + +const useSearchItems = (filterText: string): UseQueryResult<(ISubscription & IRoom)[] | undefined, Error> => { + const [, mention, name] = useMemo(() => filterText.match(/(@|#)?(.*)/i) || [], [filterText]); + const query = useMemo(() => { + const filterRegex = new RegExp(escapeRegExp(name), 'i'); + + return { + $or: [{ name: filterRegex }, { fname: filterRegex }], + ...(mention && { + t: mention === '@' ? 'd' : { $ne: 'd' }, + }), + }; + }, [name, mention]); + + const localRooms = useUserSubscriptions(query, options); + + const usernamesFromClient = [...localRooms?.map(({ t, name }) => (t === 'd' ? name : null))].filter(Boolean) as string[]; + + const searchForChannels = mention === '#'; + const searchForDMs = mention === '@'; + + const type = useMemo(() => { + if (searchForChannels) { + return { users: false, rooms: true, includeFederatedRooms: true }; + } + if (searchForDMs) { + return { users: true, rooms: false }; + } + return { users: true, rooms: true, includeFederatedRooms: true }; + }, [searchForChannels, searchForDMs]); + + const getSpotlight = useMethod('spotlight'); + + return useQuery( + ['sidebar/search/spotlight', name, usernamesFromClient, type, localRooms.map(({ _id, name }) => _id + name)], + async () => { + if (localRooms.length === LIMIT) { + return localRooms; + } + + const spotlight = await getSpotlight(name, usernamesFromClient, type); + + const filterUsersUnique = ({ _id }: { _id: string }, index: number, arr: { _id: string }[]): boolean => + index === arr.findIndex((user) => _id === user._id); + + const roomFilter = (room: { t: string; uids?: string[]; _id: string; name?: string }): boolean => + !localRooms.find( + (item) => + (room.t === 'd' && room.uids && room.uids.length > 1 && room.uids?.includes(item._id)) || + [item.rid, item._id].includes(room._id), + ); + const usersFilter = (user: { _id: string }): boolean => + !localRooms.find((room) => room.t === 'd' && room.uids && room.uids?.length === 2 && room.uids.includes(user._id)); + + const userMap = (user: { + _id: string; + name: string; + username: string; + avatarETag?: string; + }): { + _id: string; + t: string; + name: string; + fname: string; + avatarETag?: string; + } => ({ + _id: user._id, + t: 'd', + name: user.username, + fname: user.name, + avatarETag: user.avatarETag, + }); + + type resultsFromServerType = { + _id: string; + t: string; + name: string; + teamMain?: boolean; + fname?: string; + avatarETag?: string | undefined; + uids?: string[] | undefined; + }[]; + + const resultsFromServer: resultsFromServerType = []; + resultsFromServer.push(...spotlight.users.filter(filterUsersUnique).filter(usersFilter).map(userMap)); + resultsFromServer.push(...spotlight.rooms.filter(roomFilter)); + + const exact = resultsFromServer?.filter((item) => [item.name, item.fname].includes(name)); + return Array.from(new Set([...exact, ...localRooms, ...resultsFromServer])); + }, + { + staleTime: 60_000, + keepPreviousData: true, + placeholderData: localRooms, + }, + ); +}; + +const useInput = (initial: string): { value: string; onChange: FormEventHandler; setValue: Dispatch> } => { + const [value, setValue] = useState(initial); + const onChange = useMutableCallback((e) => { + setValue(e.currentTarget.value); + }); + return { value, onChange, setValue }; +}; + +const toggleSelectionState = (next: HTMLElement, current: HTMLElement | undefined, input: HTMLElement | undefined): void => { + input?.setAttribute('aria-activedescendant', next.id); + next.setAttribute('aria-selected', 'true'); + next.classList.add('rcx-sidebar-item--selected'); + if (current) { + current.removeAttribute('aria-selected'); + current.classList.remove('rcx-sidebar-item--selected'); + } +}; + +type SearchListProps = { + onClose: () => void; +}; + +const SearchList = forwardRef(function SearchList({ onClose }: SearchListProps, ref: ForwardedRef) { + const listId = useUniqueId(); + const t = useTranslation(); + const { setValue: setFilterValue, ...filter } = useInput(''); + + const cursorRef = useRef(null); + const autofocus: Ref = useMergedRefs(useAutoFocus(), cursorRef); + + const listRef = useRef(null); + const boxRef = useRef(null); + + const selectedElement: MutableRefObject = useRef(null); + const itemIndexRef = useRef(0); + + const sidebarViewMode = useUserPreference('sidebarViewMode'); + const useRealName = useSetting('UI_Use_Real_Name'); + + const sideBarItemTemplate = useTemplateByViewMode(); + const avatarTemplate = useAvatarTemplate(); + + const extended = sidebarViewMode === 'extended'; + + const filterText = useDebouncedValue(filter.value, 100); + + const placeholder = [t('Search'), shortcut].filter(Boolean).join(' '); + + const { data: items = [], isLoading } = useSearchItems(filterText); + + const itemData = useMemo( + () => ({ + items, + t, + SideBarItemTemplate: sideBarItemTemplate, + avatarTemplate, + useRealName, + extended, + sidebarViewMode, + }), + [avatarTemplate, extended, items, useRealName, sideBarItemTemplate, sidebarViewMode, t], + ); + + const changeSelection = useMutableCallback((dir) => { + let nextSelectedElement = null; + + if (dir === 'up') { + const potentialElement = selectedElement.current?.parentElement?.previousSibling as HTMLElement; + if (potentialElement) { + nextSelectedElement = potentialElement.querySelector('a'); + } + } else { + const potentialElement = selectedElement.current?.parentElement?.nextSibling as HTMLElement; + if (potentialElement) { + nextSelectedElement = potentialElement.querySelector('a'); + } + } + + if (nextSelectedElement) { + toggleSelectionState(nextSelectedElement, selectedElement.current || undefined, cursorRef?.current || undefined); + return nextSelectedElement; + } + return selectedElement.current; + }); + + const resetCursor = useMutableCallback(() => { + setTimeout(() => { + itemIndexRef.current = 0; + listRef.current?.scrollToIndex({ index: itemIndexRef.current }); + selectedElement.current = boxRef.current?.querySelector('a.rcx-sidebar-item'); + if (selectedElement.current) { + toggleSelectionState(selectedElement.current, undefined, cursorRef?.current || undefined); + } + }, 0); + }); + + usePreventDefault(boxRef); + + useEffect(() => { + resetCursor(); + }); + + useEffect(() => { + resetCursor(); + }, [filterText, resetCursor]); + + useEffect(() => { + if (!cursorRef?.current) { + return; + } + return tinykeys(cursorRef?.current, { + Escape: (event) => { + event.preventDefault(); + setFilterValue((value) => { + if (!value) { + onClose(); + } + resetCursor(); + return ''; + }); + }, + Tab: onClose, + ArrowUp: () => { + const currentElement = changeSelection('up'); + itemIndexRef.current = Math.max(itemIndexRef.current - 1, 0); + listRef.current?.scrollToIndex({ index: itemIndexRef.current }); + selectedElement.current = currentElement; + }, + ArrowDown: () => { + const currentElement = changeSelection('down'); + itemIndexRef.current = Math.min(itemIndexRef.current + 1, items.length + 1); + listRef.current?.scrollToIndex({ index: itemIndexRef.current }); + selectedElement.current = currentElement; + }, + Enter: (event) => { + event.preventDefault(); + if (selectedElement.current && items.length > 0) { + selectedElement.current.click(); + } else { + onClose(); + } + }, + }); + }, [cursorRef, changeSelection, items.length, onClose, resetCursor, setFilterValue]); + + const handleClick: MouseEventHandler = (e): void => { + if (e.target instanceof Element && [e.target.tagName, e.target.parentElement?.tagName].includes('BUTTON')) { + return; + } + return onClose(); + }; + + return ( + + + } + /> + + + room._id} + itemContent={(_, data): ReactElement => } + ref={listRef} + /> + + + ); +}); + +export default SearchList; diff --git a/apps/meteor/client/sidebarv2/search/UserItem.tsx b/apps/meteor/client/sidebarv2/search/UserItem.tsx new file mode 100644 index 0000000000000..8b9667913311c --- /dev/null +++ b/apps/meteor/client/sidebarv2/search/UserItem.tsx @@ -0,0 +1,46 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { Sidebar } from '@rocket.chat/fuselage'; +import React, { memo } from 'react'; + +import { ReactiveUserStatus } from '../../components/UserStatus'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; + +type UserItemProps = { + item: { + name?: string; + fname?: string; + _id: IUser['_id']; + t: string; + }; + t: (value: string) => string; + SideBarItemTemplate: any; + AvatarTemplate: any; + id: string; + style?: CSSStyleRule; + useRealName?: boolean; +}; + +const UserItem = ({ item, id, style, t, SideBarItemTemplate, AvatarTemplate, useRealName }: UserItemProps) => { + const title = useRealName ? item.fname || item.name : item.name || item.fname; + const icon = ( + + + + ); + const href = roomCoordinator.getRouteLink(item.t, { name: item.name }); + + return ( + } + icon={icon} + /> + ); +}; + +export default memo(UserItem); diff --git a/apps/meteor/client/sidebarv2/sections/StatusDisabledSection.tsx b/apps/meteor/client/sidebarv2/sections/StatusDisabledSection.tsx new file mode 100644 index 0000000000000..50367d7db3e5e --- /dev/null +++ b/apps/meteor/client/sidebarv2/sections/StatusDisabledSection.tsx @@ -0,0 +1,23 @@ +import { SidebarBanner } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { useStatusDisabledModal } from '../../views/admin/customUserStatus/hooks/useStatusDisabledModal'; + +type StatusDisabledSectionProps = { onDismiss: () => void }; + +const StatusDisabledSection = ({ onDismiss }: StatusDisabledSectionProps) => { + const t = useTranslation(); + const handleStatusDisabledModal = useStatusDisabledModal(); + + return ( + + ); +}; + +export default StatusDisabledSection; diff --git a/apps/meteor/client/views/room/Header/FederatedRoomOriginServer.tsx b/apps/meteor/client/views/room/Header/FederatedRoomOriginServer.tsx index 811619f7f4500..1f22f64667196 100644 --- a/apps/meteor/client/views/room/Header/FederatedRoomOriginServer.tsx +++ b/apps/meteor/client/views/room/Header/FederatedRoomOriginServer.tsx @@ -1,7 +1,7 @@ -import { HeaderTag, HeaderTagIcon } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; +import { HeaderTag, HeaderTagIcon } from '../../../components/Header'; import type { IRoomWithFederationOriginalName } from '../contexts/RoomContext'; type FederatedRoomProps = { diff --git a/apps/meteor/client/views/room/Header/Header.tsx b/apps/meteor/client/views/room/Header/Header.tsx index c350544e8157a..298076c65ce62 100644 --- a/apps/meteor/client/views/room/Header/Header.tsx +++ b/apps/meteor/client/views/room/Header/Header.tsx @@ -1,10 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isVoipRoom } from '@rocket.chat/core-typings'; -import { HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { lazy, memo, useMemo } from 'react'; +import { HeaderToolbar } from '../../../components/Header'; import SidebarToggler from '../../../components/SidebarToggler'; const OmnichannelRoomHeader = lazy(() => import('./Omnichannel/OmnichannelRoomHeader')); diff --git a/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx b/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx index ded978bd63350..10de9166d9647 100644 --- a/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx +++ b/apps/meteor/client/views/room/Header/HeaderIconWithRoom.tsx @@ -1,9 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { HeaderIcon } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; import React from 'react'; +import { HeaderIcon } from '../../../components/Header'; import { OmnichannelRoomIcon } from '../../../components/RoomIcon/OmnichannelRoomIcon'; import { useRoomIcon } from '../../../hooks/useRoomIcon'; diff --git a/apps/meteor/client/views/room/Header/HeaderSkeleton.tsx b/apps/meteor/client/views/room/Header/HeaderSkeleton.tsx index 222be6cce5aac..af69131ea897f 100644 --- a/apps/meteor/client/views/room/Header/HeaderSkeleton.tsx +++ b/apps/meteor/client/views/room/Header/HeaderSkeleton.tsx @@ -1,7 +1,8 @@ import { Skeleton } from '@rocket.chat/fuselage'; -import { Header, HeaderAvatar, HeaderContent, HeaderContentRow } from '@rocket.chat/ui-client'; import React from 'react'; +import { Header, HeaderAvatar, HeaderContent, HeaderContentRow } from '../../../components/Header'; + const HeaderSkeleton = () => { return (
diff --git a/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx b/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx index 31a913ecec7b0..54470a2b0f644 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/BackButton.tsx @@ -1,9 +1,10 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { HeaderToolbarAction } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { HeaderToolbarAction } from '../../../../components/Header'; + export const BackButton = ({ routeName }: { routeName?: string }): ReactElement => { const router = useRouter(); const t = useTranslation(); diff --git a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx index 2d54fb1ec4786..e6ae9a3747d7c 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx @@ -1,8 +1,8 @@ -import { HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; import React, { useCallback, useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; +import { HeaderToolbar } from '../../../../components/Header'; import SidebarToggler from '../../../../components/SidebarToggler'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; import RoomHeader from '../RoomHeader'; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx index b26a4d36e2481..b1da1323df650 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx @@ -1,9 +1,9 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { Box, Dropdown, Option } from '@rocket.chat/fuselage'; -import { HeaderToolbarAction } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo, useRef } from 'react'; +import { HeaderToolbarAction } from '../../../../../components/Header'; import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; import type { QuickActionsActionOptions } from '../../../lib/quickActions'; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx index 3ebf79b81d44c..13805850a13d4 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx @@ -1,9 +1,9 @@ import type { Box } from '@rocket.chat/fuselage'; -import { HeaderToolbar, HeaderToolbarAction, HeaderToolbarDivider } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo } from 'react'; +import { HeaderToolbar, HeaderToolbarAction, HeaderToolbarDivider } from '../../../../../components/Header'; import { useOmnichannelRoom } from '../../../contexts/RoomContext'; import QuickActionOptions from './QuickActionOptions'; import { useQuickActions } from './hooks/useQuickActions'; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx index 05a28f07f5c4c..235d50ffa499d 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/VoipRoomHeader.tsx @@ -1,9 +1,9 @@ import type { IVoipRoom } from '@rocket.chat/core-typings'; -import { HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; import React, { useCallback, useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; +import { HeaderToolbar } from '../../../../components/Header'; import SidebarToggler from '../../../../components/SidebarToggler'; import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; import type { RoomHeaderProps } from '../RoomHeader'; diff --git a/apps/meteor/client/views/room/Header/ParentRoom.tsx b/apps/meteor/client/views/room/Header/ParentRoom.tsx index 3d598cce4c268..5907b30190845 100644 --- a/apps/meteor/client/views/room/Header/ParentRoom.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoom.tsx @@ -1,8 +1,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { HeaderTag, HeaderTagIcon } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; import React from 'react'; +import { HeaderTag, HeaderTagIcon } from '../../../components/Header'; import { useRoomIcon } from '../../../hooks/useRoomIcon'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; diff --git a/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx b/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx index 491d26be14349..06571dd02cce5 100644 --- a/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoomWithEndpointData.tsx @@ -1,8 +1,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { HeaderTagSkeleton } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; import React from 'react'; +import { HeaderTagSkeleton } from '../../../components/Header'; import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; import ParentRoom from './ParentRoom'; diff --git a/apps/meteor/client/views/room/Header/ParentTeam.tsx b/apps/meteor/client/views/room/Header/ParentTeam.tsx index 33ef98bbe81b2..ed4a4588ef218 100644 --- a/apps/meteor/client/views/room/Header/ParentTeam.tsx +++ b/apps/meteor/client/views/room/Header/ParentTeam.tsx @@ -1,11 +1,11 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; -import { HeaderTag, HeaderTagIcon, HeaderTagSkeleton } from '@rocket.chat/ui-client'; import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; +import { HeaderTag, HeaderTagIcon, HeaderTagSkeleton } from '../../../components/Header'; import { goToRoomById } from '../../../lib/utils/goToRoomById'; type APIErrorResult = { success: boolean; error: string }; diff --git a/apps/meteor/client/views/room/Header/RoomHeader.tsx b/apps/meteor/client/views/room/Header/RoomHeader.tsx index fee9be6a55a68..2e38e2110bbeb 100644 --- a/apps/meteor/client/views/room/Header/RoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/RoomHeader.tsx @@ -1,10 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; -import { Header, HeaderAvatar, HeaderContent, HeaderContentRow, HeaderSubtitle, HeaderToolbar } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { Suspense } from 'react'; +import { Header, HeaderAvatar, HeaderContent, HeaderContentRow, HeaderSubtitle, HeaderToolbar } from '../../../components/Header'; import MarkdownText from '../../../components/MarkdownText'; import FederatedRoomOriginServer from './FederatedRoomOriginServer'; import ParentRoomWithData from './ParentRoomWithData'; diff --git a/apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx b/apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx index 2b868c28882d5..c9bfe325be92d 100644 --- a/apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx +++ b/apps/meteor/client/views/room/Header/RoomHeaderE2EESetup.tsx @@ -1,29 +1,23 @@ -import { isDirectMessageRoom } from '@rocket.chat/core-typings'; import React, { lazy } from 'react'; import { E2EEState } from '../../../../app/e2e/client/E2EEState'; import { E2ERoomState } from '../../../../app/e2e/client/E2ERoomState'; import { useE2EERoomState } from '../hooks/useE2EERoomState'; import { useE2EEState } from '../hooks/useE2EEState'; -import DirectRoomHeader from './DirectRoomHeader'; import RoomHeader from './RoomHeader'; import type { RoomHeaderProps } from './RoomHeader'; const RoomToolboxE2EESetup = lazy(() => import('./RoomToolbox/RoomToolboxE2EESetup')); -const RoomHeaderE2EESetup = ({ room, topic = '', slots = {} }: RoomHeaderProps) => { +const RoomHeaderE2EESetup = ({ room, slots = {} }: RoomHeaderProps) => { const e2eeState = useE2EEState(); const e2eRoomState = useE2EERoomState(room._id); if (e2eeState === E2EEState.SAVE_PASSWORD || e2eeState === E2EEState.ENTER_PASSWORD || e2eRoomState === E2ERoomState.WAITING_KEYS) { - return } />; + return } />; } - if (isDirectMessageRoom(room) && (room.uids?.length ?? 0) < 3) { - return ; - } - - return ; + return ; }; export default RoomHeaderE2EESetup; diff --git a/apps/meteor/client/views/room/Header/RoomTitle.tsx b/apps/meteor/client/views/room/Header/RoomTitle.tsx index 4d81d077c1542..9a3e810a46b76 100644 --- a/apps/meteor/client/views/room/Header/RoomTitle.tsx +++ b/apps/meteor/client/views/room/Header/RoomTitle.tsx @@ -1,9 +1,10 @@ import { isTeamRoom, type IRoom } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { HeaderTitle, HeaderTitleButton, useDocumentTitle } from '@rocket.chat/ui-client'; +import { useDocumentTitle } from '@rocket.chat/ui-client'; import type { KeyboardEvent, ReactElement } from 'react'; import React from 'react'; +import { HeaderTitle, HeaderTitleButton } from '../../../components/Header'; import { useRoomToolbox } from '../contexts/RoomToolboxContext'; import HeaderIconWithRoom from './HeaderIconWithRoom'; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx index a12dcb25b8262..bdda6f33f0e34 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx @@ -1,12 +1,12 @@ import type { Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { HeaderToolbarAction, HeaderToolbarDivider } from '@rocket.chat/ui-client'; import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo } from 'react'; import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; +import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx index 366f7322a135b..58e1f9f59ef78 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx @@ -1,8 +1,8 @@ import { useStableArray } from '@rocket.chat/fuselage-hooks'; -import { HeaderToolbarAction } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { HeaderToolbarAction } from '../../../../components/Header'; import { roomActionHooksForE2EESetup } from '../../../../ui'; import { useRoom } from '../../contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index bd380c5d8af2f..5de5581351980 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -1,10 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; -import { HeaderState } from '@rocket.chat/ui-client'; import { useSetting, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; +import { HeaderState } from '../../../../components/Header'; import { dispatchToastMessage } from '../../../../lib/toast'; const Encrypted = ({ room }: { room: IRoom }) => { diff --git a/apps/meteor/client/views/room/Header/icons/Favorite.tsx b/apps/meteor/client/views/room/Header/icons/Favorite.tsx index 1023a04947c3a..f6d17cb0e7b7f 100644 --- a/apps/meteor/client/views/room/Header/icons/Favorite.tsx +++ b/apps/meteor/client/views/room/Header/icons/Favorite.tsx @@ -1,9 +1,9 @@ import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { HeaderState } from '@rocket.chat/ui-client'; import { useSetting, useMethod, useTranslation, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; +import { HeaderState } from '../../../../components/Header'; import { useUserIsSubscribed } from '../../contexts/RoomContext'; const Favorite = ({ room: { _id, f: favorite = false, t: type, name } }: { room: IRoom & { f?: ISubscription['f'] } }) => { diff --git a/apps/meteor/client/views/room/Header/icons/Translate.tsx b/apps/meteor/client/views/room/Header/icons/Translate.tsx index e4b394bfbbbb6..701de69cb6793 100644 --- a/apps/meteor/client/views/room/Header/icons/Translate.tsx +++ b/apps/meteor/client/views/room/Header/icons/Translate.tsx @@ -1,8 +1,9 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { HeaderState } from '@rocket.chat/ui-client'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; +import { HeaderState } from '../../../../components/Header'; + type TranslateProps = { room: IRoom; }; diff --git a/apps/meteor/client/views/room/HeaderV2/FederatedRoomOriginServer.tsx b/apps/meteor/client/views/room/HeaderV2/FederatedRoomOriginServer.tsx new file mode 100644 index 0000000000000..a8731663b28bf --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/FederatedRoomOriginServer.tsx @@ -0,0 +1,24 @@ +import type { ReactElement } from 'react'; +import React, { useMemo } from 'react'; + +import { HeaderTag, HeaderTagIcon } from '../../../components/Header'; +import type { IRoomWithFederationOriginalName } from '../contexts/RoomContext'; + +type FederatedRoomOriginServerProps = { + room: IRoomWithFederationOriginalName; +}; + +const FederatedRoomOriginServer = ({ room }: FederatedRoomOriginServerProps): ReactElement | null => { + const originServerName = useMemo(() => room.federationOriginalName?.split(':')[1], [room.federationOriginalName]); + if (!originServerName) { + return null; + } + return ( + + + {originServerName} + + ); +}; + +export default FederatedRoomOriginServer; diff --git a/apps/meteor/client/views/room/HeaderV2/Header.tsx b/apps/meteor/client/views/room/HeaderV2/Header.tsx new file mode 100644 index 0000000000000..25394b703280b --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Header.tsx @@ -0,0 +1,55 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { isVoipRoom } from '@rocket.chat/core-typings'; +import { useLayout, useSetting } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { lazy, memo, useMemo } from 'react'; + +import { HeaderToolbar } from '../../../components/Header'; +import SidebarToggler from '../../../components/SidebarToggler'; + +const OmnichannelRoomHeader = lazy(() => import('./Omnichannel/OmnichannelRoomHeader')); +const VoipRoomHeader = lazy(() => import('./Omnichannel/VoipRoomHeader')); +const RoomHeaderE2EESetup = lazy(() => import('./RoomHeaderE2EESetup')); +const RoomHeader = lazy(() => import('./RoomHeader')); + +type HeaderProps = { + room: IRoom; +}; + +const Header = ({ room }: HeaderProps): ReactElement | null => { + const { isMobile, isEmbedded, showTopNavbarEmbeddedLayout } = useLayout(); + const encrypted = Boolean(room.encrypted); + const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages'); + const shouldDisplayE2EESetup = encrypted && !unencryptedMessagesAllowed; + + const slots = useMemo( + () => ({ + start: isMobile && ( + + + + ), + }), + [isMobile], + ); + + if (isEmbedded && !showTopNavbarEmbeddedLayout) { + return null; + } + + if (room.t === 'l') { + return ; + } + + if (isVoipRoom(room)) { + return ; + } + + if (shouldDisplayE2EESetup) { + return ; + } + + return ; +}; + +export default memo(Header); diff --git a/apps/meteor/client/views/room/HeaderV2/HeaderIconWithRoom.tsx b/apps/meteor/client/views/room/HeaderV2/HeaderIconWithRoom.tsx new file mode 100644 index 0000000000000..91cb397f30ccd --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/HeaderIconWithRoom.tsx @@ -0,0 +1,23 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { HeaderIcon } from '../../../components/Header'; +import { OmnichannelRoomIcon } from '../../../components/RoomIcon/OmnichannelRoomIcon'; +import { useRoomIcon } from '../../../hooks/useRoomIcon'; + +type HeaderIconWithRoomProps = { + room: IRoom; +}; + +const HeaderIconWithRoom = ({ room }: HeaderIconWithRoomProps): ReactElement => { + const icon = useRoomIcon(room); + if (isOmnichannelRoom(room)) { + return ; + } + + return ; +}; + +export default HeaderIconWithRoom; diff --git a/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx b/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx new file mode 100644 index 0000000000000..2c14c154c0071 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx @@ -0,0 +1,21 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { Header, HeaderAvatar, HeaderContent, HeaderContentRow } from '../../../components/Header'; + +const HeaderSkeleton = () => { + return ( +
+ + + + + + + + +
+ ); +}; + +export default HeaderSkeleton; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/BackButton.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/BackButton.tsx new file mode 100644 index 0000000000000..fc9ef65593d81 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/BackButton.tsx @@ -0,0 +1,35 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { HeaderToolbarAction } from '../../../../components/Header'; + +type BackButtonProps = { routeName?: string }; + +const BackButton = ({ routeName }: BackButtonProps): ReactElement => { + const router = useRouter(); + const t = useTranslation(); + + const back = useMutableCallback(() => { + switch (routeName) { + case 'omnichannel-directory': + router.navigate({ + name: 'omnichannel-directory', + params: { + ...router.getRouteParameters(), + bar: 'info', + }, + }); + break; + + case 'omnichannel-current-chats': + router.navigate({ name: 'omnichannel-current-chats' }); + break; + } + }); + + return ; +}; + +export default BackButton; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx new file mode 100644 index 0000000000000..3e48343188947 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/OmnichannelRoomHeader.tsx @@ -0,0 +1,55 @@ +import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; +import React, { useCallback, useMemo } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; + +import { HeaderToolbar } from '../../../../components/Header'; +import SidebarToggler from '../../../../components/SidebarToggler'; +import { useOmnichannelRoom } from '../../contexts/RoomContext'; +import RoomHeader from '../RoomHeader'; +import BackButton from './BackButton'; +import QuickActions from './QuickActions'; + +type OmnichannelRoomHeaderProps = { + slots: { + start?: unknown; + preContent?: unknown; + insideContent?: unknown; + posContent?: unknown; + end?: unknown; + toolbox?: { + pre?: unknown; + content?: unknown; + pos?: unknown; + }; + }; +}; + +const OmnichannelRoomHeader = ({ slots: parentSlot }: OmnichannelRoomHeaderProps) => { + const router = useRouter(); + + const currentRouteName = useSyncExternalStore( + router.subscribeToRouteChange, + useCallback(() => router.getRouteName(), [router]), + ); + + const { isMobile } = useLayout(); + const room = useOmnichannelRoom(); + + const slots = useMemo( + () => ({ + ...parentSlot, + start: (!!isMobile || currentRouteName === 'omnichannel-directory' || currentRouteName === 'omnichannel-current-chats') && ( + + {isMobile && } + + + ), + posContent: , + }), + [isMobile, currentRouteName, parentSlot], + ); + + return ; +}; + +export default OmnichannelRoomHeader; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/QuickActionOptions.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/QuickActionOptions.tsx new file mode 100644 index 0000000000000..b1da1323df650 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/QuickActionOptions.tsx @@ -0,0 +1,48 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Box, Dropdown, Option } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { memo, useRef } from 'react'; + +import { HeaderToolbarAction } from '../../../../../components/Header'; +import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; +import type { QuickActionsActionOptions } from '../../../lib/quickActions'; + +type QuickActionOptionsProps = { + options: QuickActionsActionOptions; + action: (id: string) => void; + room: IOmnichannelRoom; +}; + +const QuickActionOptions = ({ options, room, action, ...props }: QuickActionOptionsProps) => { + const t = useTranslation(); + const reference = useRef(null); + const target = useRef(null); + const { isVisible, toggle } = useDropdownVisibility({ reference, target }); + + const handleClick = (id: string) => (): void => { + toggle(); + action(id); + }; + + return ( + <> + toggle()} secondary={isVisible} {...props} /> + {isVisible && ( + + {options.map(({ id, label, validate }) => { + const { value: valid = true, tooltip } = validate?.(room) || {}; + return ( + + ); + })} + + )} + + ); +}; + +export default memo(QuickActionOptions); diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/QuickActions.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/QuickActions.tsx new file mode 100644 index 0000000000000..13805850a13d4 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/QuickActions.tsx @@ -0,0 +1,46 @@ +import type { Box } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps } from 'react'; +import React, { memo } from 'react'; + +import { HeaderToolbar, HeaderToolbarAction, HeaderToolbarDivider } from '../../../../../components/Header'; +import { useOmnichannelRoom } from '../../../contexts/RoomContext'; +import QuickActionOptions from './QuickActionOptions'; +import { useQuickActions } from './hooks/useQuickActions'; + +type QuickActionsProps = { + className?: ComponentProps['className']; +}; + +const QuickActions = ({ className }: QuickActionsProps) => { + const t = useTranslation(); + const room = useOmnichannelRoom(); + const { quickActions, actionDefault } = useQuickActions(); + + return ( + + {quickActions.map(({ id, color, icon, title, action = actionDefault, options }, index) => { + const props = { + id, + icon, + color, + title: t(title), + className, + index, + primary: false, + action, + room, + }; + + if (options) { + return ; + } + + return ; + })} + {quickActions.length > 0 && } + + ); +}; + +export default memo(QuickActions); diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/usePutChatOnHoldMutation.ts b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/usePutChatOnHoldMutation.ts new file mode 100644 index 0000000000000..0c9dbc767952b --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/usePutChatOnHoldMutation.ts @@ -0,0 +1,27 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +export const usePutChatOnHoldMutation = ( + options?: Omit, 'mutationFn'>, +): UseMutationResult => { + const putChatOnHold = useEndpoint('POST', '/v1/livechat/room.onHold'); + + const queryClient = useQueryClient(); + + return useMutation( + async (rid) => { + await putChatOnHold({ roomId: rid }); + }, + { + ...options, + onSuccess: async (data, rid, context) => { + await queryClient.invalidateQueries(['current-chats']); + await queryClient.invalidateQueries(['rooms', rid]); + await queryClient.invalidateQueries(['subscriptions', { rid }]); + return options?.onSuccess?.(data, rid, context); + }, + }, + ); +}; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx new file mode 100644 index 0000000000000..7446d0630b09f --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -0,0 +1,360 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { + useSetModal, + useToastMessageDispatch, + useUserId, + useSetting, + usePermission, + useRole, + useEndpoint, + useMethod, + useTranslation, + useRouter, +} from '@rocket.chat/ui-contexts'; +import React, { useCallback, useState, useEffect } from 'react'; + +import PlaceChatOnHoldModal from '../../../../../../../app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; +import { LivechatInquiry } from '../../../../../../../app/livechat/client/collections/LivechatInquiry'; +import { LegacyRoomManager } from '../../../../../../../app/ui-utils/client'; +import CloseChatModal from '../../../../../../components/Omnichannel/modals/CloseChatModal'; +import CloseChatModalData from '../../../../../../components/Omnichannel/modals/CloseChatModalData'; +import ForwardChatModal from '../../../../../../components/Omnichannel/modals/ForwardChatModal'; +import ReturnChatQueueModal from '../../../../../../components/Omnichannel/modals/ReturnChatQueueModal'; +import TranscriptModal from '../../../../../../components/Omnichannel/modals/TranscriptModal'; +import { useIsRoomOverMacLimit } from '../../../../../../hooks/omnichannel/useIsRoomOverMacLimit'; +import { useOmnichannelRouteConfig } from '../../../../../../hooks/omnichannel/useOmnichannelRouteConfig'; +import { useHasLicenseModule } from '../../../../../../hooks/useHasLicenseModule'; +import { quickActionHooks } from '../../../../../../ui'; +import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; +import type { QuickActionsActionConfig } from '../../../../lib/quickActions'; +import { QuickActionsEnum } from '../../../../lib/quickActions'; +import { usePutChatOnHoldMutation } from './usePutChatOnHoldMutation'; +import { useReturnChatToQueueMutation } from './useReturnChatToQueueMutation'; + +export const useQuickActions = (): { + quickActions: QuickActionsActionConfig[]; + actionDefault: (actionId: string) => void; +} => { + const room = useOmnichannelRoom(); + const setModal = useSetModal(); + const router = useRouter(); + + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [onHoldModalActive, setOnHoldModalActive] = useState(false); + + const visitorRoomId = room.v._id; + const rid = room._id; + const uid = useUserId(); + const roomLastMessage = room.lastMessage; + + const getVisitorInfo = useEndpoint('GET', '/v1/livechat/visitors.info'); + + const getVisitorEmail = useMutableCallback(async () => { + if (!visitorRoomId) { + return; + } + + const { + visitor: { visitorEmails }, + } = await getVisitorInfo({ visitorId: visitorRoomId }); + + if (visitorEmails?.length && visitorEmails[0].address) { + return visitorEmails[0].address; + } + }); + + useEffect(() => { + if (onHoldModalActive && roomLastMessage?.token) { + setModal(null); + } + }, [roomLastMessage, onHoldModalActive, setModal]); + + const closeModal = useCallback(() => setModal(null), [setModal]); + + const requestTranscript = useEndpoint('POST', '/v1/livechat/transcript/:rid', { rid }); + + const handleRequestTranscript = useCallback( + async (email: string, subject: string) => { + try { + await requestTranscript({ email, subject }); + closeModal(); + dispatchToastMessage({ + type: 'success', + message: t('Livechat_email_transcript_has_been_requested'), + }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, + [closeModal, dispatchToastMessage, requestTranscript, t], + ); + + const sendTranscriptPDF = useEndpoint('POST', '/v1/omnichannel/:rid/request-transcript', { rid }); + + const handleSendTranscriptPDF = useCallback(async () => { + try { + await sendTranscriptPDF(); + dispatchToastMessage({ + type: 'success', + message: t('Livechat_transcript_has_been_requested'), + }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, [dispatchToastMessage, sendTranscriptPDF, t]); + + const sendTranscript = useMethod('livechat:sendTranscript'); + + const handleSendTranscript = useCallback( + async (email: string, subject: string, token: string) => { + try { + await sendTranscript(token, rid, email, subject); + closeModal(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, + [closeModal, dispatchToastMessage, rid, sendTranscript], + ); + + const discardTranscript = useEndpoint('DELETE', '/v1/livechat/transcript/:rid', { rid }); + + const handleDiscardTranscript = useCallback(async () => { + try { + await discardTranscript(); + dispatchToastMessage({ + type: 'success', + message: t('Livechat_transcript_request_has_been_canceled'), + }); + closeModal(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, [closeModal, discardTranscript, dispatchToastMessage, t]); + + const forwardChat = useEndpoint('POST', '/v1/livechat/room.forward'); + + const handleForwardChat = useCallback( + async (departmentId?: string, userId?: string, comment?: string) => { + if (departmentId && userId) { + return; + } + const transferData: { + roomId: string; + clientAction: boolean; + comment?: string; + departmentId?: string; + userId?: string; + } = { + roomId: rid, + comment, + clientAction: true, + }; + + if (departmentId) { + transferData.departmentId = departmentId; + } + if (userId) { + transferData.userId = userId; + } + + try { + await forwardChat(transferData); + dispatchToastMessage({ type: 'success', message: t('Transferred') }); + router.navigate('/home'); + LegacyRoomManager.close(room.t + rid); + closeModal(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, + [closeModal, dispatchToastMessage, forwardChat, room.t, rid, router, t], + ); + + const closeChat = useEndpoint('POST', '/v1/livechat/room.closeByUser'); + + const handleClose = useCallback( + async ( + comment?: string, + tags?: string[], + preferences?: { omnichannelTranscriptPDF: boolean; omnichannelTranscriptEmail: boolean }, + requestData?: { email: string; subject: string }, + ) => { + try { + await closeChat({ + rid, + ...(comment && { comment }), + ...(tags && { tags }), + ...(preferences?.omnichannelTranscriptPDF && { generateTranscriptPdf: true }), + ...(preferences?.omnichannelTranscriptEmail && requestData + ? { + transcriptEmail: { + sendToVisitor: preferences?.omnichannelTranscriptEmail, + requestData, + }, + } + : { transcriptEmail: { sendToVisitor: false } }), + }); + LivechatInquiry.remove({ rid }); + closeModal(); + dispatchToastMessage({ type: 'success', message: t('Chat_closed_successfully') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, + [closeChat, closeModal, dispatchToastMessage, rid, t], + ); + + const returnChatToQueueMutation = useReturnChatToQueueMutation({ + onSuccess: () => { + LegacyRoomManager.close(room.t + rid); + router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + closeModal(); + }, + }); + + const putChatOnHoldMutation = usePutChatOnHoldMutation({ + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Chat_On_Hold_Successfully') }); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + closeModal(); + }, + }); + + const handleAction = useMutableCallback(async (id: string) => { + switch (id) { + case QuickActionsEnum.MoveQueue: + setModal( + returnChatToQueueMutation.mutate(rid)} + onCancel={(): void => { + closeModal(); + }} + />, + ); + break; + case QuickActionsEnum.TranscriptPDF: + handleSendTranscriptPDF(); + break; + case QuickActionsEnum.TranscriptEmail: + const visitorEmail = await getVisitorEmail(); + + if (!visitorEmail) { + dispatchToastMessage({ type: 'error', message: t('Customer_without_registered_email') }); + break; + } + + setModal( + , + ); + break; + case QuickActionsEnum.ChatForward: + setModal(); + break; + case QuickActionsEnum.CloseChat: + const email = await getVisitorEmail(); + setModal( + room.departmentId ? ( + + ) : ( + + ), + ); + break; + case QuickActionsEnum.OnHoldChat: + setModal( + putChatOnHoldMutation.mutate(rid)} + onCancel={(): void => { + closeModal(); + setOnHoldModalActive(false); + }} + />, + ); + setOnHoldModalActive(true); + break; + default: + break; + } + }); + + const omnichannelRouteConfig = useOmnichannelRouteConfig(); + + const manualOnHoldAllowed = useSetting('Livechat_allow_manual_on_hold'); + + const hasManagerRole = useRole('livechat-manager'); + const hasMonitorRole = useRole('livechat-monitor'); + + const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole || hasMonitorRole) && room?.lastMessage?.t !== 'livechat-close'; + const canMoveQueue = !!omnichannelRouteConfig?.returnQueue && room?.u !== undefined; + const canForwardGuest = usePermission('transfer-livechat-guest'); + const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript'); + const hasLicense = useHasLicenseModule('livechat-enterprise'); + const canSendTranscriptPDF = usePermission('request-pdf-transcript'); + const canCloseRoom = usePermission('close-livechat-room'); + const canCloseOthersRoom = usePermission('close-others-livechat-room'); + const restrictedOnHold = useSetting('Livechat_allow_manual_on_hold_upon_agent_engagement_only'); + const canRoomBePlacedOnHold = !room.onHold && room.u; + const canAgentPlaceOnHold = !room.lastMessage?.token; + const canPlaceChatOnHold = Boolean(manualOnHoldAllowed && canRoomBePlacedOnHold && (!restrictedOnHold || canAgentPlaceOnHold)); + const isRoomOverMacLimit = useIsRoomOverMacLimit(room); + + const hasPermissionButtons = (id: string): boolean => { + switch (id) { + case QuickActionsEnum.MoveQueue: + return !isRoomOverMacLimit && !!roomOpen && canMoveQueue; + case QuickActionsEnum.ChatForward: + return !isRoomOverMacLimit && !!roomOpen && canForwardGuest; + case QuickActionsEnum.Transcript: + return !isRoomOverMacLimit && (canSendTranscriptEmail || (hasLicense && canSendTranscriptPDF)); + case QuickActionsEnum.TranscriptEmail: + return !isRoomOverMacLimit && canSendTranscriptEmail; + case QuickActionsEnum.TranscriptPDF: + return hasLicense && !isRoomOverMacLimit && canSendTranscriptPDF; + case QuickActionsEnum.CloseChat: + return !!roomOpen && (canCloseRoom || canCloseOthersRoom); + case QuickActionsEnum.OnHoldChat: + return !!roomOpen && canPlaceChatOnHold; + default: + break; + } + return false; + }; + + const quickActions = quickActionHooks + .map((quickActionHook) => quickActionHook()) + .filter((quickAction): quickAction is QuickActionsActionConfig => !!quickAction) + .filter((action) => { + const { options, id } = action; + if (options) { + action.options = options.filter(({ id }) => hasPermissionButtons(id)); + } + + return hasPermissionButtons(id); + }) + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + + const actionDefault = useMutableCallback((actionId: string) => { + handleAction(actionId); + }); + + return { quickActions, actionDefault }; +}; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts new file mode 100644 index 0000000000000..c037f200514bf --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts @@ -0,0 +1,28 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useMethod } from '@rocket.chat/ui-contexts'; +import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +export const useReturnChatToQueueMutation = ( + options?: Omit, 'mutationFn'>, +): UseMutationResult => { + const returnChatToQueue = useMethod('livechat:returnAsInquiry'); + + const queryClient = useQueryClient(); + + return useMutation( + async (rid) => { + await returnChatToQueue(rid); + }, + { + ...options, + onSuccess: async (data, rid, context) => { + await queryClient.invalidateQueries(['current-chats']); + await queryClient.removeQueries(['rooms', rid]); + await queryClient.removeQueries(['/v1/rooms.info', rid]); + await queryClient.removeQueries(['subscriptions', { rid }]); + return options?.onSuccess?.(data, rid, context); + }, + }, + ); +}; diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/index.ts b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/index.ts new file mode 100644 index 0000000000000..5ec9f10150e34 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export default lazy(() => import('./QuickActions')); diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx new file mode 100644 index 0000000000000..c79f8999f1a88 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/VoipRoomHeader.tsx @@ -0,0 +1,42 @@ +import type { IVoipRoom } from '@rocket.chat/core-typings'; +import { useLayout, useRouter } from '@rocket.chat/ui-contexts'; +import React, { useCallback, useMemo } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; + +import { HeaderToolbar } from '../../../../components/Header'; +import SidebarToggler from '../../../../components/SidebarToggler'; +import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhoneNumber'; +import type { RoomHeaderProps } from '../RoomHeader'; +import RoomHeader from '../RoomHeader'; +import BackButton from './BackButton'; + +type VoipRoomHeaderProps = { + room: IVoipRoom; +} & Omit; + +const VoipRoomHeader = ({ slots: parentSlot, room }: VoipRoomHeaderProps) => { + const router = useRouter(); + + const currentRouteName = useSyncExternalStore( + router.subscribeToRouteChange, + useCallback(() => router.getRouteName(), [router]), + ); + + const { isMobile } = useLayout(); + + const slots = useMemo( + () => ({ + ...parentSlot, + start: (!!isMobile || currentRouteName === 'omnichannel-directory') && ( + + {isMobile && } + {currentRouteName === 'omnichannel-directory' && } + + ), + }), + [isMobile, currentRouteName, parentSlot], + ); + return ; +}; + +export default VoipRoomHeader; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx new file mode 100644 index 0000000000000..0c53d790caf8a --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx @@ -0,0 +1,30 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import React from 'react'; + +import { HeaderTag, HeaderTagIcon } from '../../../components/Header'; +import { useRoomIcon } from '../../../hooks/useRoomIcon'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; + +type ParentRoomProps = { + room: Pick; +}; + +const ParentRoom = ({ room }: ParentRoomProps) => { + const icon = useRoomIcon(room); + + const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); + + return ( + (e.code === 'Space' || e.code === 'Enter') && handleRedirect()} + onClick={handleRedirect} + > + + {roomCoordinator.getRoomName(room.t, room)} + + ); +}; + +export default ParentRoom; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx new file mode 100644 index 0000000000000..aed3adc53b39c --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx @@ -0,0 +1,28 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useUserSubscription } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import ParentRoom from './ParentRoom'; +import ParentRoomWithEndpointData from './ParentRoomWithEndpointData'; + +type ParentRoomWithDataProps = { + room: IRoom; +}; + +const ParentRoomWithData = ({ room }: ParentRoomWithDataProps) => { + const { prid } = room; + + if (!prid) { + throw new Error('Parent room ID is missing'); + } + + const subscription = useUserSubscription(prid); + + if (subscription) { + return ; + } + + return ; +}; + +export default ParentRoomWithData; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx new file mode 100644 index 0000000000000..89d0ea1f82204 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx @@ -0,0 +1,26 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import React from 'react'; + +import { HeaderTagSkeleton } from '../../../components/Header'; +import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; +import ParentRoom from './ParentRoom'; + +type ParentRoomWithEndpointDataProps = { + rid: IRoom['_id']; +}; + +const ParentRoomWithEndpointData = ({ rid }: ParentRoomWithEndpointDataProps) => { + const { data, isLoading, isError } = useRoomInfoEndpoint(rid); + + if (isLoading) { + return ; + } + + if (isError || !data?.room) { + return null; + } + + return ; +}; + +export default ParentRoomWithEndpointData; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx b/apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx new file mode 100644 index 0000000000000..2f8bfa57c5665 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx @@ -0,0 +1,79 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { TEAM_TYPE } from '@rocket.chat/core-typings'; +import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; + +import { HeaderTag, HeaderTagIcon, HeaderTagSkeleton } from '../../../components/Header'; +import { goToRoomById } from '../../../lib/utils/goToRoomById'; + +type APIErrorResult = { success: boolean; error: string }; + +type ParentTeamProps = { + room: IRoom; +}; + +const ParentTeam = ({ room }: ParentTeamProps) => { + const { teamId } = room; + const userId = useUserId(); + + if (!teamId) { + throw new Error('invalid rid'); + } + + if (!userId) { + throw new Error('invalid uid'); + } + + const teamsInfoEndpoint = useEndpoint('GET', '/v1/teams.info'); + const userTeamsListEndpoint = useEndpoint('GET', '/v1/users.listTeams'); + + const { + data: teamInfoData, + isLoading: teamInfoLoading, + isError: teamInfoError, + } = useQuery(['teamId', teamId], async () => teamsInfoEndpoint({ teamId }), { + keepPreviousData: true, + retry: (_, error) => (error as APIErrorResult)?.error === 'unauthorized' && false, + }); + + const { data: userTeams, isLoading: userTeamsLoading } = useQuery(['userId', userId], async () => userTeamsListEndpoint({ userId })); + + const userBelongsToTeam = userTeams?.teams?.find((team) => team._id === teamId) || false; + const isTeamPublic = teamInfoData?.teamInfo.type === TEAM_TYPE.PUBLIC; + + const redirectToMainRoom = (): void => { + const rid = teamInfoData?.teamInfo.roomId; + if (!rid) { + return; + } + + if (!(isTeamPublic || userBelongsToTeam)) { + return; + } + + goToRoomById(rid); + }; + + if (teamInfoLoading || userTeamsLoading) { + return ; + } + + if (teamInfoError) { + return null; + } + + return ( + (e.code === 'Space' || e.code === 'Enter') && redirectToMainRoom()} + onClick={redirectToMainRoom} + > + + {teamInfoData?.teamInfo.name} + + ); +}; + +export default ParentTeam; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx new file mode 100644 index 0000000000000..8ef21aecf0cda --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx @@ -0,0 +1,69 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { RoomAvatar } from '@rocket.chat/ui-avatar'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { Suspense } from 'react'; + +import { Header, HeaderAvatar, HeaderContent, HeaderContentRow, HeaderToolbar } from '../../../components/Header'; +import FederatedRoomOriginServer from './FederatedRoomOriginServer'; +import ParentRoomWithData from './ParentRoomWithData'; +import ParentTeam from './ParentTeam'; +import RoomTitle from './RoomTitle'; +import RoomToolbox from './RoomToolbox'; +import Encrypted from './icons/Encrypted'; +import Favorite from './icons/Favorite'; +import Translate from './icons/Translate'; + +export type RoomHeaderProps = { + room: IRoom; + slots: { + start?: unknown; + preContent?: unknown; + insideContent?: unknown; + posContent?: unknown; + end?: unknown; + toolbox?: { + pre?: unknown; + content?: unknown; + pos?: unknown; + }; + }; + roomToolbox?: JSX.Element; +}; + +const RoomHeader = ({ room, slots = {}, roomToolbox }: RoomHeaderProps) => { + const t = useTranslation(); + + return ( +
+ {slots?.start} + + + + {slots?.preContent} + + + + + {room.prid && } + {room.teamId && !room.teamMain && } + {isRoomFederated(room) && } + + + {slots?.insideContent} + + + {slots?.posContent} + + + {slots?.toolbox?.pre} + {slots?.toolbox?.content || roomToolbox || } + {slots?.toolbox?.pos} + + + {slots?.end} +
+ ); +}; + +export default RoomHeader; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx new file mode 100644 index 0000000000000..c9bfe325be92d --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeaderE2EESetup.tsx @@ -0,0 +1,23 @@ +import React, { lazy } from 'react'; + +import { E2EEState } from '../../../../app/e2e/client/E2EEState'; +import { E2ERoomState } from '../../../../app/e2e/client/E2ERoomState'; +import { useE2EERoomState } from '../hooks/useE2EERoomState'; +import { useE2EEState } from '../hooks/useE2EEState'; +import RoomHeader from './RoomHeader'; +import type { RoomHeaderProps } from './RoomHeader'; + +const RoomToolboxE2EESetup = lazy(() => import('./RoomToolbox/RoomToolboxE2EESetup')); + +const RoomHeaderE2EESetup = ({ room, slots = {} }: RoomHeaderProps) => { + const e2eeState = useE2EEState(); + const e2eRoomState = useE2EERoomState(room._id); + + if (e2eeState === E2EEState.SAVE_PASSWORD || e2eeState === E2EEState.ENTER_PASSWORD || e2eRoomState === E2ERoomState.WAITING_KEYS) { + return } />; + } + + return ; +}; + +export default RoomHeaderE2EESetup; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomLeader.tsx b/apps/meteor/client/views/room/HeaderV2/RoomLeader.tsx new file mode 100644 index 0000000000000..cfe1f620afbf3 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomLeader.tsx @@ -0,0 +1,64 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { css } from '@rocket.chat/css-in-js'; +import { Box, IconButton } from '@rocket.chat/fuselage'; +import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { UIEvent } from 'react'; +import React, { useCallback, useMemo } from 'react'; + +import { HeaderSubtitle } from '../../../components/Header'; +import { ReactiveUserStatus } from '../../../components/UserStatus'; +import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; +import { useUserCard } from '../contexts/UserCardContext'; + +type RoomLeaderProps = { + _id: IUser['_id']; + name: IUser['name']; + username?: IUser['username']; +}; + +const RoomLeader = ({ _id, name, username }: RoomLeaderProps) => { + const t = useTranslation(); + + const { openUserCard, triggerProps } = useUserCard(); + + const onAvatarClick = useCallback( + (event: UIEvent, username: IUser['username']) => { + if (!username) { + return; + } + + openUserCard(event, username); + }, + [openUserCard], + ); + + const chatNowLink = useMemo(() => roomCoordinator.getRouteLink('d', { name: username }) || undefined, [username]); + + if (!username) { + throw new Error('username is required'); + } + + const roomLeaderStyle = css` + display: flex; + align-items: center; + flex-shrink: 0; + flex-grow: 0; + gap: 4px; + + [role='button'] { + cursor: pointer; + } + `; + + return ( + + onAvatarClick(event, username)} {...triggerProps} /> + + {name} + + + ); +}; + +export default RoomLeader; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx new file mode 100644 index 0000000000000..d728f8c03a3f3 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx @@ -0,0 +1,54 @@ +import { isTeamRoom, type IRoom } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useDocumentTitle } from '@rocket.chat/ui-client'; +import type { KeyboardEvent } from 'react'; +import React from 'react'; + +import { HeaderTitle, HeaderTitleButton } from '../../../components/Header'; +import { useRoomToolbox } from '../contexts/RoomToolboxContext'; +import HeaderIconWithRoom from './HeaderIconWithRoom'; + +type RoomTitleProps = { room: IRoom }; + +const RoomTitle = ({ room }: RoomTitleProps) => { + useDocumentTitle(room.name, false); + const { openTab } = useRoomToolbox(); + + const handleOpenRoomInfo = useEffectEvent(() => { + if (isTeamRoom(room)) { + return openTab('team-info'); + } + + switch (room.t) { + case 'l': + openTab('room-info'); + break; + + case 'v': + openTab('voip-room-info'); + break; + + case 'd': + (room.uids?.length ?? 0) > 2 ? openTab('user-info-group') : openTab('user-info'); + break; + + default: + openTab('channel-settings'); + break; + } + }); + + return ( + (e.code === 'Enter' || e.code === 'Space') && handleOpenRoomInfo()} + onClick={() => handleOpenRoomInfo()} + tabIndex={0} + role='button' + > + + {room.name} + + ); +}; + +export default RoomTitle; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx new file mode 100644 index 0000000000000..5fda368711c1e --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx @@ -0,0 +1,101 @@ +import type { Box } from '@rocket.chat/fuselage'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps } from 'react'; +import React, { memo } from 'react'; + +import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; +import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; +import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; +import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; +import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; + +type RoomToolboxProps = { + className?: ComponentProps['className']; +}; + +type MenuActionsProps = { + id: string; + items: GenericMenuItemProps[]; +}[]; + +const RoomToolbox = ({ className }: RoomToolboxProps) => { + const t = useTranslation(); + const { roomToolboxExpanded } = useLayout(); + + const toolbox = useRoomToolbox(); + const { actions, openTab } = toolbox; + + const featuredActions = actions.filter((action) => action.featured); + const normalActions = actions.filter((action) => !action.featured); + const visibleActions = !roomToolboxExpanded ? [] : normalActions.slice(0, 6); + + const hiddenActions = (!roomToolboxExpanded ? actions : normalActions.slice(6)) + .filter((item) => !item.disabled && !item.featured) + .map((item) => ({ + 'key': item.id, + 'content': t(item.title), + 'onClick': + item.action ?? + ((): void => { + openTab(item.id); + }), + 'data-qa-id': `ToolBoxAction-${item.icon}`, + ...item, + })) + .reduce((acc, item) => { + const group = item.type ? item.type : ''; + const section = acc.find((section: { id: string }) => section.id === group); + if (section) { + section.items.push(item); + return acc; + } + + const newSection = { id: group, key: item.key, title: group === 'apps' ? t('Apps') : '', items: [item] }; + acc.push(newSection); + + return acc; + }, [] as MenuActionsProps); + + const renderDefaultToolboxItem: RoomToolboxActionConfig['renderToolboxItem'] = useEffectEvent( + ({ id, className, index, icon, title, toolbox: { tab }, action, disabled, tooltip }) => { + return ( + + ); + }, + ); + + const mapToToolboxItem = (action: RoomToolboxActionConfig, index: number) => { + return (action.renderToolboxItem ?? renderDefaultToolboxItem)?.({ + ...action, + action: action.action ?? (() => toolbox.openTab(action.id)), + className, + index, + toolbox, + }); + }; + + return ( + <> + {featuredActions.map(mapToToolboxItem)} + {featuredActions.length > 0 && } + {visibleActions.map(mapToToolboxItem)} + {(normalActions.length > 6 || !roomToolboxExpanded) && !!hiddenActions.length && ( + + )} + + ); +}; + +export default memo(RoomToolbox); diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx new file mode 100644 index 0000000000000..9c6afa33fc277 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx @@ -0,0 +1,41 @@ +import { useStableArray } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { HeaderToolbarAction } from '../../../../components/Header'; +import { roomActionHooksForE2EESetup } from '../../../../ui'; +import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; +import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; + +const RoomToolboxE2EESetup = () => { + const t = useTranslation(); + const toolbox = useRoomToolbox(); + + const { tab } = toolbox; + + const actions = useStableArray( + roomActionHooksForE2EESetup + .map((roomActionHook) => roomActionHook()) + .filter((roomAction): roomAction is RoomToolboxActionConfig => !!roomAction), + ); + + return ( + <> + {actions.map(({ id, icon, title, action, disabled, tooltip }, index) => ( + toolbox.openTab(id))} + disabled={disabled} + tooltip={tooltip} + /> + ))} + + ); +}; + +export default RoomToolboxE2EESetup; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/index.ts b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/index.ts new file mode 100644 index 0000000000000..d5a042756df45 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export default lazy(() => import('./RoomToolbox')); diff --git a/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx b/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx new file mode 100644 index 0000000000000..5de5581351980 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx @@ -0,0 +1,36 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import colors from '@rocket.chat/fuselage-tokens/colors'; +import { useSetting, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import React, { memo } from 'react'; + +import { HeaderState } from '../../../../components/Header'; +import { dispatchToastMessage } from '../../../../lib/toast'; + +const Encrypted = ({ room }: { room: IRoom }) => { + const t = useTranslation(); + const e2eEnabled = useSetting('E2E_Enable'); + const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); + const canToggleE2E = usePermission('toggle-room-e2e-encryption'); + const encryptedLabel = canToggleE2E ? t('Encrypted_key_title') : t('Encrypted'); + const handleE2EClick = useMutableCallback(async () => { + if (!canToggleE2E) { + return; + } + + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + if (!success) { + return; + } + + dispatchToastMessage({ + type: 'success', + message: t('E2E_Encryption_disabled_for_room', { roomName: room.name }), + }); + }); + return e2eEnabled && room?.encrypted ? ( + + ) : null; +}; + +export default memo(Encrypted); diff --git a/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx b/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx new file mode 100644 index 0000000000000..f6d17cb0e7b7f --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx @@ -0,0 +1,52 @@ +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useSetting, useMethod, useTranslation, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import React, { memo } from 'react'; + +import { HeaderState } from '../../../../components/Header'; +import { useUserIsSubscribed } from '../../contexts/RoomContext'; + +const Favorite = ({ room: { _id, f: favorite = false, t: type, name } }: { room: IRoom & { f?: ISubscription['f'] } }) => { + const t = useTranslation(); + const subscribed = useUserIsSubscribed(); + const dispatchToastMessage = useToastMessageDispatch(); + + const isFavoritesEnabled = useSetting('Favorite_Rooms') && ['c', 'p', 'd', 't'].includes(type); + const toggleFavorite = useMethod('toggleFavorite'); + + const handleFavoriteClick = useEffectEvent(() => { + if (!isFavoritesEnabled) { + return; + } + + try { + toggleFavorite(_id, !favorite); + dispatchToastMessage({ + type: 'success', + message: !favorite + ? t('__roomName__was_added_to_favorites', { roomName: name }) + : t('__roomName__was_removed_from_favorites', { roomName: name }), + }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const favoriteLabel = favorite ? `${t('Unfavorite')} ${name}` : `${t('Favorite')} ${name}`; + + if (!subscribed || !isFavoritesEnabled) { + return null; + } + + return ( + + ); +}; + +export default memo(Favorite); diff --git a/apps/meteor/client/views/room/HeaderV2/icons/Translate.tsx b/apps/meteor/client/views/room/HeaderV2/icons/Translate.tsx new file mode 100644 index 0000000000000..0097c9f2e3d8c --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/icons/Translate.tsx @@ -0,0 +1,21 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import type { FC } from 'react'; +import React, { memo } from 'react'; + +import { HeaderState } from '../../../../components/Header'; + +type TranslateProps = { + room: IRoom; +}; + +const Translate: FC = ({ room: { autoTranslateLanguage, autoTranslate } }) => { + const t = useTranslation(); + const autoTranslateEnabled = useSetting('AutoTranslate_Enabled'); + const encryptedLabel = t('Translated'); + return autoTranslateEnabled && autoTranslate && autoTranslateLanguage ? ( + + ) : null; +}; + +export default memo(Translate); diff --git a/apps/meteor/client/views/room/HeaderV2/index.ts b/apps/meteor/client/views/room/HeaderV2/index.ts new file mode 100644 index 0000000000000..a38c9709c31bc --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/index.ts @@ -0,0 +1 @@ +export { default as HeaderV2 } from './Header'; diff --git a/apps/meteor/client/views/room/Room.tsx b/apps/meteor/client/views/room/Room.tsx index 6bbeb9f9e230b..55f8b4a82be8c 100644 --- a/apps/meteor/client/views/room/Room.tsx +++ b/apps/meteor/client/views/room/Room.tsx @@ -1,3 +1,4 @@ +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { createElement, lazy, memo, Suspense } from 'react'; @@ -7,8 +8,10 @@ import { ErrorBoundary } from 'react-error-boundary'; import { ContextualbarSkeleton } from '../../components/Contextualbar'; import RoomE2EESetup from './E2EESetup/RoomE2EESetup'; import Header from './Header'; +import { HeaderV2 } from './HeaderV2'; import MessageHighlightProvider from './MessageList/providers/MessageHighlightProvider'; import RoomBody from './body/RoomBody'; +import RoomBodyV2 from './body/RoomBodyV2'; import { useRoom } from './contexts/RoomContext'; import { useRoomToolbox } from './contexts/RoomToolboxContext'; import { useAppsContextualBar } from './hooks/useAppsContextualBar'; @@ -40,8 +43,34 @@ const Room = (): ReactElement => { ? t('Conversation_with__roomName__', { roomName: room.name }) : t('Channel__roomName__', { roomName: room.name }) } - header={
} - body={shouldDisplayE2EESetup ? : } + header={ + <> + + + + + +
+ + + + } + body={ + shouldDisplayE2EESetup ? ( + + ) : ( + <> + + + + + + + + + + ) + } aside={ (toolbox.tab?.tabComponent && ( diff --git a/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx b/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx new file mode 100644 index 0000000000000..f9daf3d148164 --- /dev/null +++ b/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx @@ -0,0 +1,14 @@ +import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; +import type { FC, MouseEvent } from 'react'; +import React from 'react'; + +type AnnouncementComponenttParams = { + onClickOpen: (e: MouseEvent) => void; +}; + +const AnnouncementComponent: FC = ({ children, onClickOpen }) => ( + + {children} + +); +export default AnnouncementComponent; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx new file mode 100644 index 0000000000000..09a6d9ed695e6 --- /dev/null +++ b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx @@ -0,0 +1,17 @@ +import { action } from '@storybook/addon-actions'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import React from 'react'; + +import RoomAnnouncement from '.'; + +export default { + title: 'Room/Announcement', + component: RoomAnnouncement, +} as ComponentMeta; + +export const Default: ComponentStory = (args) => ; +Default.storyName = 'Announcement'; +Default.args = { + announcement: 'Lorem Ipsum Indolor', + announcementDetails: action('announcementDetails'), +}; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx new file mode 100644 index 0000000000000..4c96b88bb3484 --- /dev/null +++ b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx @@ -0,0 +1,47 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; +import type { FC, MouseEvent } from 'react'; +import React from 'react'; + +import GenericModal from '../../../components/GenericModal'; +import MarkdownText from '../../../components/MarkdownText'; +import AnnouncementComponent from './AnnouncementComponent'; + +type RoomAnnouncementParams = { + announcement: string; + announcementDetails?: () => void; +}; + +const RoomAnnouncement: FC = ({ announcement, announcementDetails }) => { + const t = useTranslation(); + const setModal = useSetModal(); + const closeModal = useMutableCallback(() => setModal(null)); + const handleClick = (e: MouseEvent): void => { + if ((e.target as HTMLAnchorElement).href) { + return; + } + + if (window?.getSelection()?.toString() !== '') { + return; + } + + announcementDetails + ? announcementDetails() + : setModal( + + + + + , + ); + }; + + return announcement ? ( + + + + ) : null; +}; + +export default RoomAnnouncement; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/index.tsx b/apps/meteor/client/views/room/RoomAnnouncement/index.tsx new file mode 100644 index 0000000000000..a6b289d12c9bd --- /dev/null +++ b/apps/meteor/client/views/room/RoomAnnouncement/index.tsx @@ -0,0 +1 @@ +export { default } from './RoomAnnouncement'; diff --git a/apps/meteor/client/views/room/RoomOpener.tsx b/apps/meteor/client/views/room/RoomOpener.tsx index 734f0434bf0d9..c30acf6f0e835 100644 --- a/apps/meteor/client/views/room/RoomOpener.tsx +++ b/apps/meteor/client/views/room/RoomOpener.tsx @@ -1,10 +1,10 @@ import type { RoomType } from '@rocket.chat/core-typings'; import { States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; -import { Header } from '@rocket.chat/ui-client'; import type { ReactElement } from 'react'; import React, { lazy, Suspense } from 'react'; import { useTranslation } from 'react-i18next'; +import { Header } from '../../components/Header'; import { getErrorMessage } from '../../lib/errorHandling'; import { NotAuthorizedError } from '../../lib/errors/NotAuthorizedError'; import { OldUrlRoomError } from '../../lib/errors/OldUrlRoomError'; diff --git a/apps/meteor/client/views/room/body/LeaderBar.tsx b/apps/meteor/client/views/room/body/LeaderBar.tsx index 2d5fdb8bbdb88..b54d37ed5df9d 100644 --- a/apps/meteor/client/views/room/body/LeaderBar.tsx +++ b/apps/meteor/client/views/room/body/LeaderBar.tsx @@ -19,7 +19,9 @@ type LeaderBarProps = { onAvatarClick?: (event: UIEvent, username: IUser['username']) => void; triggerProps: AriaButtonProps<'button'>; }; - +/** + * @deprecated on newNavigation feature. Remove after full migration. + */ const LeaderBar = ({ _id, name, username, visible, onAvatarClick, triggerProps }: LeaderBarProps): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx new file mode 100644 index 0000000000000..32b4288b3b0e8 --- /dev/null +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -0,0 +1,300 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; +import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import type { MouseEventHandler, ReactElement } from 'react'; +import React, { memo, useCallback, useMemo, useRef } from 'react'; + +import { isTruthy } from '../../../../lib/isTruthy'; +import { CustomScrollbars } from '../../../components/CustomScrollbars'; +import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; +import { BubbleDate } from '../BubbleDate'; +import { MessageList } from '../MessageList'; +import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; +import RoomAnnouncement from '../RoomAnnouncement'; +import ComposerContainer from '../composer/ComposerContainer'; +import RoomComposer from '../composer/RoomComposer/RoomComposer'; +import { useChat } from '../contexts/ChatContext'; +import { useRoom, useRoomSubscription, useRoomMessages } from '../contexts/RoomContext'; +import { useRoomToolbox } from '../contexts/RoomToolboxContext'; +import { useDateScroll } from '../hooks/useDateScroll'; +import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; +import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; +import DropTargetOverlay from './DropTargetOverlay'; +import JumpToRecentMessageButton from './JumpToRecentMessageButton'; +import LoadingMessagesIndicator from './LoadingMessagesIndicator'; +import RetentionPolicyWarning from './RetentionPolicyWarning'; +import RoomForeword from './RoomForeword/RoomForeword'; +import { RoomTopic } from './RoomTopic'; +import UnreadMessagesIndicator from './UnreadMessagesIndicator'; +import UploadProgressIndicator from './UploadProgressIndicator'; +import { useBannerSection } from './hooks/useBannerSection'; +import { useFileUpload } from './hooks/useFileUpload'; +import { useGetMore } from './hooks/useGetMore'; +import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; +import { useHasNewMessages } from './hooks/useHasNewMessages'; +import { useListIsAtBottom } from './hooks/useListIsAtBottom'; +import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; +import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; +import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; +import { useHandleUnread } from './hooks/useUnreadMessages'; + +const RoomBody = (): ReactElement => { + const chat = useChat(); + if (!chat) { + throw new Error('No ChatContext provided'); + } + + const t = useTranslation(); + const isLayoutEmbedded = useEmbeddedLayout(); + const room = useRoom(); + const user = useUser(); + const toolbox = useRoomToolbox(); + const admin = useRole('admin'); + const subscription = useRoomSubscription(); + + const retentionPolicy = useRetentionPolicy(room); + + const hideFlexTab = useUserPreference('hideFlexTab') || undefined; + const hideUsernames = useUserPreference('hideUsernames'); + const displayAvatars = useUserPreference('displayAvatars'); + + const { hasMorePreviousMessages, hasMoreNextMessages, isLoadingMoreMessages } = useRoomMessages(); + + const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead') as boolean | undefined; + + const canPreviewChannelRoom = usePermission('preview-c-room'); + + const subscribed = !!subscription; + + const canPreview = useMemo(() => { + if (room && room.t !== 'c') { + return true; + } + + if (allowAnonymousRead === true) { + return true; + } + + if (canPreviewChannelRoom) { + return true; + } + + return subscribed; + }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); + + const innerBoxRef = useRef(null); + + const { + wrapperRef: unreadBarWrapperRef, + innerRef: unreadBarInnerRef, + handleUnreadBarJumpToButtonClick, + handleMarkAsReadButtonClick, + counter: [unread], + } = useHandleUnread(room, subscription); + + const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); + + const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom } = useListIsAtBottom(); + + const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + + const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection(); + + const { + uploads, + handleUploadFiles, + handleUploadProgressClose, + targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], + } = useFileUpload(); + + const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(room._id); + + const { messageListRef } = useMessageListNavigation(); + + const { handleNewMessageButtonClick, handleJumpToRecentButtonClick, handleComposerResize, hasNewMessages, newMessagesScrollRef } = + useHasNewMessages(room._id, user?._id, atBottomRef, { + sendToBottom, + sendToBottomIfNecessary, + isAtBottom, + }); + + const innerRef = useMergedRefs( + dateScrollInnerRef, + innerBoxRef, + restoreScrollPositionInnerRef, + isAtBottomInnerRef, + newMessagesScrollRef, + sectionScrollRef, + unreadBarInnerRef, + getMoreInnerRef, + + messageListRef, + ); + + const wrapperBoxRefs = useMergedRefs(unreadBarWrapperRef); + + const handleNavigateToPreviousMessage = useCallback((): void => { + chat.messageEditing.toPreviousMessage(); + }, [chat.messageEditing]); + + const handleNavigateToNextMessage = useCallback((): void => { + chat.messageEditing.toNextMessage(); + }, [chat.messageEditing]); + + const handleCloseFlexTab: MouseEventHandler = useCallback( + (e): void => { + /* + * check if the element is a button or anchor + * it considers the role as well + * usually, the flex tab is closed when clicking outside of it + * but if the user clicks on a button or anchor, we don't want to close the flex tab + * because the user could be actually trying to open the flex tab through those elements + */ + + const checkElement = (element: HTMLElement | null): boolean => { + if (!element) { + return false; + } + if (element instanceof HTMLButtonElement || element.getAttribute('role') === 'button') { + return true; + } + if (element instanceof HTMLAnchorElement || element.getAttribute('role') === 'link') { + return true; + } + return checkElement(element.parentElement); + }; + + if (checkElement(e.target as HTMLElement)) { + return; + } + + toolbox.closeTab(); + }, + [toolbox], + ); + + useGoToHomeOnRemoved(room, user?._id); + useReadMessageWindowEvents(); + useQuoteMessageByUrl(); + + const wrapperStyle = css` + position: absolute; + width: 100%; + z-index: 5; + top: 0px; + + &.animated-hidden { + top: -88px; + } + `; + + return ( + <> + + + + {!isLayoutEmbedded && room.announcement && } + + + + + + + + ); +}; + +export default memo(RoomBody); diff --git a/apps/meteor/client/views/room/body/RoomTopic.tsx b/apps/meteor/client/views/room/body/RoomTopic.tsx new file mode 100644 index 0000000000000..fd385aff9fa6e --- /dev/null +++ b/apps/meteor/client/views/room/body/RoomTopic.tsx @@ -0,0 +1,67 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { isTeamRoom } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; +import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; +import { useSetting, useUserId, useTranslation, useRouter } from '@rocket.chat/ui-contexts'; +import React from 'react'; + +import { RoomRoles } from '../../../../app/models/client'; +import MarkdownText from '../../../components/MarkdownText'; +import { usePresence } from '../../../hooks/usePresence'; +import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; +import RoomLeader from '../HeaderV2/RoomLeader'; +import { useCanEditRoom } from '../contextualBar/Info/hooks/useCanEditRoom'; + +type RoomTopicProps = { + room: IRoom; + user: IUser | null; +}; + +export const RoomTopic = ({ room, user }: RoomTopicProps) => { + const t = useTranslation(); + const canEdit = useCanEditRoom(room); + const userId = useUserId(); + const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); + const directUserData = usePresence(directUserId); + const useRealName = useSetting('UI_Use_Real_Name') as boolean; + const router = useRouter(); + + const currentRoute = router.getLocationPathname(); + const href = isTeamRoom(room) ? `${currentRoute}/team-info` : `${currentRoute}/channel-settings`; + + const { data: roomLeader } = useReactiveQuery(['rooms', room._id, 'leader', { not: user?._id }], () => { + const leaderRoomRole = RoomRoles.findOne({ + 'rid': room._id, + 'roles': 'leader', + 'u._id': { $ne: user?._id }, + }); + + if (!leaderRoomRole) { + return null; + } + + return { + ...leaderRoomRole.u, + name: useRealName ? leaderRoomRole.u.name || leaderRoomRole.u.username : leaderRoomRole.u.username, + }; + }); + + const topic = room.t === 'd' && (room.uids?.length ?? 0) < 3 ? directUserData?.statusText : room.topic; + + if (!topic && !roomLeader) return null; + + return ( + + + {roomLeader && !topic && canEdit ? ( + + {t('Add_topic')} + + ) : ( + + )} + + {roomLeader && } + + ); +}; diff --git a/apps/meteor/client/views/room/body/hooks/useBannerSection.ts b/apps/meteor/client/views/room/body/hooks/useBannerSection.ts new file mode 100644 index 0000000000000..0c174c1218a71 --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useBannerSection.ts @@ -0,0 +1,44 @@ +import { useCallback, useRef, useState } from 'react'; + +import { isAtBottom } from '../../../../../app/ui/client/views/app/lib/scrolling'; +import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; + +export const useBannerSection = () => { + const [hideSection, setHideSection] = useState(false); + + const wrapperBoxRef = useRef(null); + + const innerScrollRef = useCallback((node: HTMLElement | null) => { + if (!node) { + return; + } + let lastScrollTopRef = 0; + + wrapperBoxRef.current?.addEventListener('mouseover', () => setHideSection(false)); + + node.addEventListener( + 'scroll', + withThrottling({ wait: 100 })((event) => { + const roomLeader = wrapperBoxRef.current?.querySelector('.rcx-header-section'); + + if (roomLeader) { + if (isAtBottom(node, 0)) { + setHideSection(false); + } else if (event.target.scrollTop < lastScrollTopRef) { + setHideSection(true); + } else if (!isAtBottom(node, 100) && event.target.scrollTop > parseFloat(getComputedStyle(roomLeader).height)) { + setHideSection(true); + } + } + lastScrollTopRef = event.target.scrollTop; + }), + { passive: true }, + ); + }, []); + + return { + wrapperRef: wrapperBoxRef, + hideSection, + innerRef: innerScrollRef, + }; +}; diff --git a/apps/meteor/client/views/room/composer/ComposerUserActionIndicator/ComposerUserActionIndicator.tsx b/apps/meteor/client/views/room/composer/ComposerUserActionIndicator/ComposerUserActionIndicator.tsx index 7e81fb2e7ccb1..e2d5c1f03fe08 100644 --- a/apps/meteor/client/views/room/composer/ComposerUserActionIndicator/ComposerUserActionIndicator.tsx +++ b/apps/meteor/client/views/room/composer/ComposerUserActionIndicator/ComposerUserActionIndicator.tsx @@ -37,7 +37,15 @@ const ComposerUserActionIndicator = ({ rid, tmid }: { rid: string; tmid?: string }, [rid, tmid]), ); return ( - + {actions.map(({ action, users }, index) => ( {index > 0 && ', '} diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx index 141ee72113054..1bcf85c702c98 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx @@ -13,6 +13,7 @@ import { ContextualbarClose, ContextualbarEmptyContent, ContextualbarTitle, + ContextualbarSection, } from '../../../../components/Contextualbar'; import { VirtuosoScrollbars } from '../../../../components/CustomScrollbars'; import { goToRoomById } from '../../../../lib/utils/goToRoomById'; @@ -61,25 +62,16 @@ function DiscussionsList({ {t('Discussions')} + + } + addon={} + /> + - - } - addon={} - /> - - {loading && ( diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx index 2edc21fe63a69..e61bd736768b9 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx @@ -8,6 +8,7 @@ import { ContextualbarHeader, ContextualbarTitle, ContextualbarIcon, + ContextualbarSection, } from '../../../../components/Contextualbar'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import MessageSearch from './components/MessageSearch'; @@ -30,13 +31,13 @@ const MessageSearchTab = () => { {t('Search_Messages')} + {providerQuery.data && ( + + + + )} - {providerQuery.isSuccess && ( - <> - - - - )} + {providerQuery.isSuccess && } {providerQuery.isError && ( {t('Search_current_provider_not_active')} diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx index 9067c4c55ebfc..b9c076e367819 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearchForm.tsx @@ -1,5 +1,5 @@ import type { IMessageSearchProvider } from '@rocket.chat/core-typings'; -import { Box, Field, FieldLabel, FieldRow, FieldHint, Icon, TextInput, ToggleSwitch, Callout } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldHint, Icon, TextInput, ToggleSwitch, Callout } from '@rocket.chat/fuselage'; import { useDebouncedCallback, useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -48,38 +48,23 @@ const MessageSearchForm = ({ provider, onSearch }: MessageSearchFormProps) => { const t = useTranslation(); return ( - - + + + } + placeholder={t('Search_Messages')} + aria-label={t('Search_Messages')} + autoComplete='off' + {...register('searchText')} + /> + {provider.description && } + + {globalSearchEnabled && ( - - } - placeholder={t('Search_Messages')} - aria-label={t('Search_Messages')} - autoComplete='off' - {...register('searchText')} - /> - - {provider.description && } + {t('Global_Search')} + - {globalSearchEnabled && ( - - - {t('Global_Search')} - - - - )} - + )} {room.encrypted && ( {t('Encrypted_RoomType', { roomType: getRoomTypeTranslation(room).toLowerCase() })} diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.tsx index b18fc41e41dac..be479ffe77bdf 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.tsx @@ -1,6 +1,6 @@ import type { IUpload, IUploadWithUser } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { Box, Icon, TextInput, Select, Throbber, Margins } from '@rocket.chat/fuselage'; +import { Box, Icon, TextInput, Select, Throbber, ContextualbarSection } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FormEvent } from 'react'; import React, { useMemo } from 'react'; @@ -63,23 +63,19 @@ const RoomFiles = ({ {t('Files')} {onClickClose && } - - - - - } - /> - - + + {loading && ( diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx index cdbd8329eaa12..214dff25dfac2 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx @@ -15,6 +15,7 @@ import { ContextualbarContent, ContextualbarFooter, ContextualbarEmptyContent, + ContextualbarSection, } from '../../../../components/Contextualbar'; import { VirtuosoScrollbars } from '../../../../components/CustomScrollbars'; import InfiniteListAnchor from '../../../../components/InfiniteListAnchor'; @@ -92,20 +93,19 @@ const RoomMembers = ({ {isTeam ? t('Teams_members') : t('Members')} {onClickClose && } - - - } - /> - - setType(value as 'online' | 'all')} value={type} options={options} /> - + + {loading && ( diff --git a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx index f2796b322040f..cb5c5875f00c9 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx @@ -1,5 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { Box, Icon, TextInput, Select, Margins, Callout, Throbber } from '@rocket.chat/fuselage'; +import { Box, Icon, TextInput, Select, Callout, Throbber } from '@rocket.chat/fuselage'; import { useResizeObserver, useAutoFocus, useLocalStorage, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useUserId } from '@rocket.chat/ui-contexts'; import type { FormEvent, ReactElement } from 'react'; @@ -13,6 +13,7 @@ import { ContextualbarIcon, ContextualbarTitle, ContextualbarEmptyContent, + ContextualbarSection, } from '../../../../components/Contextualbar'; import { VirtuosoScrollbars } from '../../../../components/CustomScrollbars'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; @@ -123,33 +124,19 @@ const ThreadList = () => { {t('Threads')} - - - - - - } - ref={autoFocusRef} - value={searchText} - onChange={handleSearchTextChange} - /> - - handleTypeChange(String(value))} /> - + + {phase === AsyncStatePhase.LOADING && ( diff --git a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebarV2.tsx b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebarV2.tsx new file mode 100644 index 0000000000000..495cd469400fb --- /dev/null +++ b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebarV2.tsx @@ -0,0 +1,67 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { IRouterPaths } from '@rocket.chat/ui-contexts'; +import { useLayout, useSetting, useCurrentModal, useCurrentRoutePath, useRouter } from '@rocket.chat/ui-contexts'; +import type { ReactElement, ReactNode } from 'react'; +import React, { useEffect, useRef } from 'react'; + +import NavBar from '../../../NavBarV2'; +import Sidebar from '../../../sidebarv2'; +import AccessibilityShortcut from './AccessibilityShortcut'; +import { MainLayoutStyleTags } from './MainLayoutStyleTags'; + +const LayoutWithSidebarV2 = ({ children }: { children: ReactNode }): ReactElement => { + const { isEmbedded: embeddedLayout } = useLayout(); + + const modal = useCurrentModal(); + const currentRoutePath = useCurrentRoutePath(); + const router = useRouter(); + const removeSidenav = embeddedLayout && !currentRoutePath?.startsWith('/admin'); + const readReceiptsEnabled = useSetting('Message_Read_Receipt_Store_Users'); + + const firstChannelAfterLogin = useSetting('First_Channel_After_Login'); + + const redirected = useRef(false); + + useEffect(() => { + const needToBeRedirect = currentRoutePath && ['/', '/home'].includes(currentRoutePath); + + if (!needToBeRedirect) { + return; + } + + if (!firstChannelAfterLogin || typeof firstChannelAfterLogin !== 'string') { + return; + } + + if (redirected.current) { + return; + } + redirected.current = true; + + router.navigate({ name: `/channel/${firstChannelAfterLogin}` as keyof IRouterPaths }); + }, [router, currentRoutePath, firstChannelAfterLogin]); + + return ( + <> + + + + + {!removeSidenav && } +
+ {children} +
+
+ + ); +}; + +export default LayoutWithSidebarV2; diff --git a/apps/meteor/client/views/root/MainLayout/MainLayoutStyleTags.tsx b/apps/meteor/client/views/root/MainLayout/MainLayoutStyleTags.tsx index c830047a3424f..662ee415fa26e 100644 --- a/apps/meteor/client/views/root/MainLayout/MainLayoutStyleTags.tsx +++ b/apps/meteor/client/views/root/MainLayout/MainLayoutStyleTags.tsx @@ -10,7 +10,7 @@ export const MainLayoutStyleTags = () => { return ( <> - + {theme === 'dark' && } ); diff --git a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx index a308a3a862969..d6fc7e9d14cf3 100644 --- a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx +++ b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx @@ -1,3 +1,4 @@ +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import { useLayout, useUser, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React, { lazy, useCallback } from 'react'; @@ -5,6 +6,7 @@ import React, { lazy, useCallback } from 'react'; import { Roles } from '../../../../app/models/client'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; import LayoutWithSidebar from './LayoutWithSidebar'; +import LayoutWithSidebarV2 from './LayoutWithSidebarV2'; const AccountSecurityPage = lazy(() => import('../../account/security/AccountSecurityPage')); @@ -34,7 +36,16 @@ const TwoFactorAuthSetupCheck = ({ children }: { children: ReactNode }): ReactEl ); } - return {children}; + return ( + + + {children} + + + {children} + + + ); }; export default TwoFactorAuthSetupCheck; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx index d82dd19b1af88..8bad02c135c80 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannels.tsx @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { Box, Icon, TextInput, Margins, Select, Throbber, ButtonGroup, Button } from '@rocket.chat/fuselage'; +import { Box, Icon, TextInput, Select, Throbber, ButtonGroup, Button } from '@rocket.chat/fuselage'; import { useMutableCallback, useAutoFocus, useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, Dispatch, SetStateAction, SyntheticEvent } from 'react'; @@ -15,6 +15,7 @@ import { ContextualbarContent, ContextualbarFooter, ContextualbarEmptyContent, + ContextualbarSection, } from '../../../../components/Contextualbar'; import { VirtuosoScrollbars } from '../../../../components/CustomScrollbars'; import InfiniteListAnchor from '../../../../components/InfiniteListAnchor'; @@ -85,23 +86,13 @@ const TeamsChannels = ({ {t('Team_Channels')} {onClickClose && } - - - - - } - /> - - setType(val as 'all' | 'autoJoin')} value={type} options={options} /> + + {loading && ( diff --git a/apps/meteor/package.json b/apps/meteor/package.json index be037850b47ad..17eef6e29f64d 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -242,7 +242,7 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.2", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.3", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.32.0", diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index ee06baf84b833..199f6b1c4388d 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -76,8 +76,8 @@ test.describe.serial('channel-management', () => { await poHomeChannel.dismissToast(); await poHomeChannel.tabs.btnRoomInfo.click(); - await expect(page.getByRole('dialog', { name: 'Channel info' })).toContainText('hello-topic-edited'); await expect(page.getByRole('heading', { name: 'hello-topic-edited' })).toBeVisible(); + await expect(page.getByRole('dialog', { name: 'Channel info' })).toContainText('hello-topic-edited'); await expect(poHomeChannel.getSystemMessageByText('changed room topic to hello-topic-edited')).toBeVisible(); }); @@ -122,7 +122,7 @@ test.describe.serial('channel-management', () => { }); test('should truncate the room name for small screens', async ({ page }) => { - const hugeName = faker.string.alpha(100); + const hugeName = faker.string.alpha(200); await poHomeChannel.sidenav.openChat(targetChannel); await poHomeChannel.tabs.btnRoomInfo.click(); await poHomeChannel.tabs.room.btnEdit.click(); diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 3e95eac85fc41..add24d33a8abb 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 83b98bdecd6e0..ee3dd4e42da7d 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -66,7 +66,7 @@ "@rocket.chat/apps-engine": "1.43.0", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "^0.36.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 315324f5a3147..03cdcd09d29fb 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/fuselage-tokens": "^0.33.1", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/styled": "~0.31.25", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 96ad85cf1ce6c..e168ec9cf7df1 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6474,6 +6474,11 @@ "Next_image": "Next Image", "Previous_image": "Previous image", "Image_gallery": "Image gallery", + "Add_topic": "Add topic", + "Chat_with_leader": "Chat with leader", "You_cant_take_chats_unavailable": "You cannot take new conversations because you're unavailable", - "You_cant_take_chats_offline": "You cannot take new conversations because you're offline" + "You_cant_take_chats_offline": "You cannot take new conversations because you're offline", + "New_navigation": "Enhanced navigation experience", + "New_navigation_description": "Explore our improved navigation, designed with clear scopes for easy access to what you need. This change serves as the foundation for future advancements in navigation management.", + "Workspace_and_user_settings": "Workspace and user settings" } diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index a433947296cb8..2e45c20f1d36a 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@babel/core": "~7.22.20", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/ui-contexts": "workspace:^", "@types/babel__core": "~7.20.3", "@types/react": "~17.0.69", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 33bff50a90c82..fe35430364cf2 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/mock-providers": "workspace:^", diff --git a/packages/ui-client/src/components/Header/HeaderToolbar/HeaderToolbarAction.tsx b/packages/ui-client/src/components/Header/HeaderToolbar/HeaderToolbarAction.tsx index 36a124ece5ab3..ba08eeba23327 100644 --- a/packages/ui-client/src/components/Header/HeaderToolbar/HeaderToolbarAction.tsx +++ b/packages/ui-client/src/components/Header/HeaderToolbar/HeaderToolbarAction.tsx @@ -1,6 +1,7 @@ import { IconButton } from '@rocket.chat/fuselage'; import { forwardRef } from 'react'; +// TODO: remove any and type correctly const HeaderToolbarAction = forwardRef(function HeaderToolbarAction( { id, icon, action, index, title, 'data-tooltip': tooltip, ...props }, ref, diff --git a/packages/ui-client/src/components/HeaderV2/Header.stories.tsx b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx new file mode 100644 index 0000000000000..5988cf24c33b8 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx @@ -0,0 +1,192 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { Avatar } from '@rocket.chat/fuselage'; +import { SettingsContext } from '@rocket.chat/ui-contexts'; +import { action } from '@storybook/addon-actions'; +import type { ComponentMeta } from '@storybook/react'; +import { ComponentProps } from 'react'; + +import { + HeaderV2 as Header, + HeaderV2Avatar as HeaderAvatar, + HeaderV2Content as HeaderContent, + HeaderV2ContentRow as HeaderContentRow, + HeaderV2Icon as HeaderIcon, + HeaderV2Toolbar as HeaderToolbar, + HeaderV2ToolbarAction as HeaderToolbarAction, + HeaderV2ToolbarActionBadge as HeaderToolbarActionBadge, + HeaderV2Title as HeaderTitle, + HeaderV2State as HeaderState, +} from '.'; +import { RoomBanner } from '../RoomBanner'; +import { RoomBannerContent } from '../RoomBanner/RoomBannerContent'; + +const avatarUrl = + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; + +export default { + title: 'Components/HeaderV2', + component: Header, + subcomponents: { + HeaderToolbar, + HeaderToolbarAction, + HeaderAvatar, + HeaderContent, + HeaderContentRow, + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + (fn) => ( + [ + () => () => undefined, + () => ({ + _id, + type: 'action', + value: '', + actionText: '', + public: true, + blocked: false, + createdAt: new Date(), + env: true, + i18nLabel: _id, + packageValue: false, + sorter: 1, + ts: new Date(), + _updatedAt: new Date(), + }), + ], + querySettings: () => [() => () => undefined, () => []], + dispatch: async () => undefined, + }} + > + {fn()} + + ), + ], +} as ComponentMeta; + +const room: IRoom = { + t: 'c', + name: 'general general general general general general general general general general general general general general general general general general general', + _id: 'GENERAL', + encrypted: true, + autoTranslate: true, + autoTranslateLanguage: 'pt-BR', + u: { + _id: 'rocket.cat', + name: 'rocket.cat', + username: 'rocket.cat', + }, + msgs: 123, + usersCount: 3, + _updatedAt: new Date(), +} as const; + +const CustomAvatar = (props: Omit, 'url'>) => ; +const icon = { name: 'hash' } as const; + +export const Default = () => ( +
+ + + + + + {icon && } + {room.name} + + + + + + + + + + +
+); + +export const WithBurger = () => ( +
+ + + + + + + + + {icon && } + {room.name} + + + + + + + + + + +
+); + +export const WithActionBadge = () => ( +
+ + + + + + {icon && } + {room.name} + + + + + + 1 + + + 2 + + + 99 + + + +
+); + +export const WithTopicBanner = () => ( + <> +
+ + + + + + {icon && } + {room.name} + + + + + + + + + + +
+ + Topic {room.name} + + +); diff --git a/packages/ui-client/src/components/HeaderV2/Header.tsx b/packages/ui-client/src/components/HeaderV2/Header.tsx new file mode 100644 index 0000000000000..4ee887e93cfbd --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/Header.tsx @@ -0,0 +1,31 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import type { ComponentPropsWithoutRef } from 'react'; + +import HeaderDivider from './HeaderDivider'; + +type HeaderProps = ComponentPropsWithoutRef; + +const Header = (props: HeaderProps) => { + const { isMobile } = useLayout(); + + return ( + + + + + ); +}; + +export default Header; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderAvatar.tsx b/packages/ui-client/src/components/HeaderV2/HeaderAvatar.tsx new file mode 100644 index 0000000000000..06b80697446c8 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderAvatar.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type HeaderAvatarProps = ComponentPropsWithoutRef; + +const HeaderAvatar = (props: HeaderAvatarProps) => ; + +export default HeaderAvatar; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderContent.tsx b/packages/ui-client/src/components/HeaderV2/HeaderContent.tsx new file mode 100644 index 0000000000000..2f8d75329493f --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderContent.tsx @@ -0,0 +1,10 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type HeaderContentProps = ComponentPropsWithoutRef; + +const HeaderContent = (props: HeaderContentProps) => ( + +); + +export default HeaderContent; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderContentRow.tsx b/packages/ui-client/src/components/HeaderV2/HeaderContentRow.tsx new file mode 100644 index 0000000000000..5b2ca14686678 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderContentRow.tsx @@ -0,0 +1,10 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type HeaderContentRowProps = ComponentPropsWithoutRef; + +const HeaderContentRow = (props: HeaderContentRowProps) => ( + +); + +export default HeaderContentRow; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderDivider.tsx b/packages/ui-client/src/components/HeaderV2/HeaderDivider.tsx new file mode 100644 index 0000000000000..2beadec2d0882 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderDivider.tsx @@ -0,0 +1,5 @@ +import { Divider } from '@rocket.chat/fuselage'; + +const HeaderDivider = () => ; + +export default HeaderDivider; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderIcon.tsx b/packages/ui-client/src/components/HeaderV2/HeaderIcon.tsx new file mode 100644 index 0000000000000..33d5d760537e7 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderIcon.tsx @@ -0,0 +1,15 @@ +import { Box, Icon } from '@rocket.chat/fuselage'; +import type { Keys as IconName } from '@rocket.chat/icons'; +import type { ReactElement } from 'react'; +import { isValidElement } from 'react'; + +type HeaderIconProps = { icon: ReactElement | { name: IconName; color?: string } | null }; + +const HeaderIcon = ({ icon }: HeaderIconProps) => + icon && ( + + {isValidElement(icon) ? icon : } + + ); + +export default HeaderIcon; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderState.tsx b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx new file mode 100644 index 0000000000000..a5f29d77b28e4 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx @@ -0,0 +1,17 @@ +import { Icon, IconButton } from '@rocket.chat/fuselage'; +import type { Keys as IconName } from '@rocket.chat/icons'; +import type { ComponentPropsWithoutRef, MouseEventHandler } from 'react'; + +type HeaderStateProps = + | (Omit, 'onClick'> & { + onClick: MouseEventHandler; + }) + | (Omit, 'name' | 'onClick'> & { + icon: IconName; + onClick?: undefined; + }); + +const HeaderState = (props: HeaderStateProps) => + props.onClick ? : ; + +export default HeaderState; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderSubtitle.tsx b/packages/ui-client/src/components/HeaderV2/HeaderSubtitle.tsx new file mode 100644 index 0000000000000..4f3ef17492819 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderSubtitle.tsx @@ -0,0 +1,10 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type HeaderSubtitleProps = ComponentPropsWithoutRef; + +const HeaderSubtitle = (props: HeaderSubtitleProps) => ( + +); + +export default HeaderSubtitle; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTag.tsx b/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTag.tsx new file mode 100644 index 0000000000000..bef9407706e45 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTag.tsx @@ -0,0 +1,14 @@ +import { Box, Tag } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +type HeaderTagProps = ComponentProps; + +const HeaderTag = ({ children, ...props }: HeaderTagProps) => ( + + + {children} + + +); + +export default HeaderTag; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTagIcon.tsx b/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTagIcon.tsx new file mode 100644 index 0000000000000..fd124ea09a10f --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTagIcon.tsx @@ -0,0 +1,23 @@ +import { Box, Icon } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; +import { isValidElement } from 'react'; + +type HeaderIconProps = { + icon: ReactElement | Pick, 'name' | 'color'> | null; +}; + +const HeaderTagIcon = ({ icon }: HeaderIconProps) => { + if (!icon) { + return null; + } + + return isValidElement(icon) ? ( + + {icon} + + ) : ( + + ); +}; + +export default HeaderTagIcon; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTagSkeleton.tsx b/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTagSkeleton.tsx new file mode 100644 index 0000000000000..14fecc6c194be --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderTag/HeaderTagSkeleton.tsx @@ -0,0 +1,5 @@ +import { Skeleton } from '@rocket.chat/fuselage'; + +const HeaderTagSkeleton = () => ; + +export default HeaderTagSkeleton; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderTag/index.ts b/packages/ui-client/src/components/HeaderV2/HeaderTag/index.ts new file mode 100644 index 0000000000000..da15ab34e601c --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderTag/index.ts @@ -0,0 +1,3 @@ +export { default as HeaderTag } from './HeaderTag'; +export { default as HeaderTagIcon } from './HeaderTagIcon'; +export { default as HeaderTagSkeleton } from './HeaderTagSkeleton'; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderTitle.tsx b/packages/ui-client/src/components/HeaderV2/HeaderTitle.tsx new file mode 100644 index 0000000000000..88ebadc2ca11b --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderTitle.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type HeaderTitleProps = ComponentPropsWithoutRef; + +const HeaderTitle = (props: HeaderTitleProps) => ; + +export default HeaderTitle; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderTitleButton.tsx b/packages/ui-client/src/components/HeaderV2/HeaderTitleButton.tsx new file mode 100644 index 0000000000000..57a43b44b3610 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderTitleButton.tsx @@ -0,0 +1,27 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Palette } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type HeaderTitleButtonProps = Omit, 'className'> & { className?: string }; + +const HeaderTitleButton = ({ className, ...props }: HeaderTitleButtonProps) => { + const customClass = css` + border-width: 1px; + border-style: solid; + border-color: transparent; + + &:hover { + cursor: pointer; + background-color: ${Palette.surface['surface-hover']}; + } + &:focus.focus-visible { + outline: 0; + box-shadow: 0 0 0 2px ${Palette.stroke['stroke-extra-light-highlight']}; + border-color: ${Palette.stroke['stroke-highlight']}; + } + `; + + return ; +}; + +export default HeaderTitleButton; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbar.tsx b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbar.tsx new file mode 100644 index 0000000000000..2c2377a5778aa --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbar.tsx @@ -0,0 +1,18 @@ +import { useToolbar } from '@react-aria/toolbar'; +import { ButtonGroup } from '@rocket.chat/fuselage'; +import { type ComponentProps, useRef } from 'react'; + +type HeaderToolbarProps = ComponentProps; + +const HeaderToolbar = (props: HeaderToolbarProps) => { + const ref = useRef(null); + const { toolbarProps } = useToolbar(props, ref); + + return ( + + {props.children} + + ); +}; + +export default HeaderToolbar; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarAction.tsx b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarAction.tsx new file mode 100644 index 0000000000000..ba08eeba23327 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarAction.tsx @@ -0,0 +1,26 @@ +import { IconButton } from '@rocket.chat/fuselage'; +import { forwardRef } from 'react'; + +// TODO: remove any and type correctly +const HeaderToolbarAction = forwardRef(function HeaderToolbarAction( + { id, icon, action, index, title, 'data-tooltip': tooltip, ...props }, + ref, +) { + return ( + action(id)} + data-toolbox={index} + key={id} + icon={icon} + small + position='relative' + overflow='visible' + {...(tooltip ? { 'data-tooltip': tooltip, 'title': '' } : { title })} + {...props} + /> + ); +}); + +export default HeaderToolbarAction; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarActionBadge.tsx b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarActionBadge.tsx new file mode 100644 index 0000000000000..8f35727fe518f --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarActionBadge.tsx @@ -0,0 +1,22 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Badge } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type HeaderToolbarActionBadgeProps = ComponentPropsWithoutRef; + +const HeaderToolbarActionBadge = (props: HeaderToolbarActionBadgeProps) => ( + + + +); + +export default HeaderToolbarActionBadge; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarDivider.tsx b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarDivider.tsx new file mode 100644 index 0000000000000..f3823002e88e1 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/HeaderToolbarDivider.tsx @@ -0,0 +1,5 @@ +import { Divider } from '@rocket.chat/fuselage'; + +const HeaderToolbarDivider = () => ; + +export default HeaderToolbarDivider; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderToolbar/index.ts b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/index.ts new file mode 100644 index 0000000000000..933f03d658c06 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/HeaderToolbar/index.ts @@ -0,0 +1,4 @@ +export { default as HeaderToolbar } from './HeaderToolbar'; +export { default as HeaderToolbarAction } from './HeaderToolbarAction'; +export { default as HeaderToolbarActionBadge } from './HeaderToolbarActionBadge'; +export { default as HeaderToolbarDivider } from './HeaderToolbarDivider'; diff --git a/packages/ui-client/src/components/HeaderV2/index.ts b/packages/ui-client/src/components/HeaderV2/index.ts new file mode 100644 index 0000000000000..2b6a829edc6f7 --- /dev/null +++ b/packages/ui-client/src/components/HeaderV2/index.ts @@ -0,0 +1,17 @@ +export { default as HeaderV2 } from './Header'; +export { default as HeaderV2Avatar } from './HeaderAvatar'; +export { default as HeaderV2Content } from './HeaderContent'; +export { default as HeaderV2ContentRow } from './HeaderContentRow'; +export { default as HeaderV2Divider } from './HeaderDivider'; +export { default as HeaderV2Icon } from './HeaderIcon'; +export { default as HeaderV2State } from './HeaderState'; +export { default as HeaderV2Subtitle } from './HeaderSubtitle'; +export { HeaderTag as HeaderV2Tag, HeaderTagIcon as HeaderV2TagIcon, HeaderTagSkeleton as HeaderV2TagSkeleton } from './HeaderTag'; +export { default as HeaderV2Title } from './HeaderTitle'; +export { default as HeaderV2TitleButton } from './HeaderTitleButton'; +export { + HeaderToolbar as HeaderV2Toolbar, + HeaderToolbarAction as HeaderV2ToolbarAction, + HeaderToolbarActionBadge as HeaderV2ToolbarActionBadge, + HeaderToolbarDivider as HeaderV2ToolbarDivider, +} from './HeaderToolbar'; diff --git a/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx b/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx new file mode 100644 index 0000000000000..90e14b18b3e2a --- /dev/null +++ b/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx @@ -0,0 +1,68 @@ +import { Avatar, Box, IconButton } from '@rocket.chat/fuselage'; +import { ComponentProps } from 'react'; + +import { RoomBanner } from './RoomBanner'; +import { RoomBannerContent } from './RoomBannerContent'; + +export default { + title: 'Components/RoomBanner', + component: RoomBanner, +}; +const avatarUrl = + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; + +const CustomAvatar = (props: Omit, 'url'>) => ; + +export const Default = () => ( + + + Plain text long long long loooooooooooooong loooong loooong loooong loooong loooong loooong teeeeeeext + + + + + Will Bell + + + + +); + +export const WithoutTopic = () => ( + + + + Add topic + + + + + + Will Bell + + + + +); + +export const TopicAndAnnouncement = () => ( +
+ + + Topic long long long loooooooooooooong loooong loooong loooong loooong loooong loooong loooong loooong teeeeeeext + + + + + Will Bell + + + + + + + Announcement banner google.com + + +
+); diff --git a/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx b/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx new file mode 100644 index 0000000000000..e5ab045803143 --- /dev/null +++ b/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx @@ -0,0 +1,39 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Divider, Palette } from '@rocket.chat/fuselage'; +import { useLayout } from '@rocket.chat/ui-contexts'; +import { ComponentProps } from 'react'; + +const clickable = css` + cursor: pointer; + &:focus-visible { + outline: ${Palette.stroke['stroke-highlight']} solid 1px; + } +`; + +export const RoomBanner = ({ onClick, className, ...props }: ComponentProps) => { + const { isMobile } = useLayout(); + + return ( + <> + + + + ); +}; diff --git a/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx b/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx new file mode 100644 index 0000000000000..65c3cd4f38f7c --- /dev/null +++ b/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx @@ -0,0 +1,6 @@ +import { Box } from '@rocket.chat/fuselage'; +import { HTMLAttributes } from 'react'; + +export const RoomBannerContent = (props: Omit, 'is'>) => ( + +); diff --git a/packages/ui-client/src/components/RoomBanner/index.ts b/packages/ui-client/src/components/RoomBanner/index.ts new file mode 100644 index 0000000000000..9c79ab469b624 --- /dev/null +++ b/packages/ui-client/src/components/RoomBanner/index.ts @@ -0,0 +1,2 @@ +export * from './RoomBanner'; +export * from './RoomBannerContent'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index f5f37ac1c8784..c98eace56f13a 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -8,5 +8,7 @@ export { default as TextSeparator } from './TextSeparator'; export * from './TooltipComponent'; export * as UserStatus from './UserStatus'; export * from './Header'; +export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; export * from './FeaturePreview/FeaturePreview'; +export * from './RoomBanner'; diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index ae202a8bd88a0..4e79eebbd13a4 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -1,7 +1,12 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; -export type FeaturesAvailable = 'quickReactions' | 'navigationBar' | 'enable-timestamp-message-parser' | 'contextualbarResizable'; +export type FeaturesAvailable = + | 'quickReactions' + | 'navigationBar' + | 'enable-timestamp-message-parser' + | 'contextualbarResizable' + | 'newNavigation'; export type FeaturePreviewProps = { name: FeaturesAvailable; @@ -47,6 +52,14 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ value: false, enabled: true, }, + { + name: 'newNavigation', + i18n: 'New_navigation', + description: 'New_navigation_description', + group: 'Navigation', + value: false, + enabled: true, + }, ]; export const enabledDefaultFeatures = defaultFeaturesPreview.filter((feature) => feature.enabled); diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index d2dc4b8080097..fdc00ad086e16 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/icons": "^0.36.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 143a312594acd..efacce7a45869 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/styled": "~0.31.25", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index a6e3b0c1de245..a9293ff218cec 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -15,7 +15,7 @@ "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.54.3", + "@rocket.chat/fuselage": "^0.55.2", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.32.0", diff --git a/yarn.lock b/yarn.lock index 161a50f1b4e5d..938f9822053bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8937,7 +8937,7 @@ __metadata: "@rocket.chat/apps-engine": 1.43.0 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/gazzodown": "workspace:^" @@ -8999,9 +8999,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.54.3": - version: 0.54.3 - resolution: "@rocket.chat/fuselage@npm:0.54.3" +"@rocket.chat/fuselage@npm:^0.55.2": + version: 0.55.2 + resolution: "@rocket.chat/fuselage@npm:0.55.2" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 @@ -9019,7 +9019,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: bec4d0b92e919103cda927520040f46004266ec5e1b3964c5bec6c6be59f8f051f2940689785f4e78984a9a18230a175b9f5f8e548f2b8f951387d567570735c + checksum: 286f4ac261621a09de74e34ef52f5c473e7c2e55ca977507ab1b1fdccf7274c2ca788a42d1415ffd4f4f629377b0bb1ed9ad70ddead7b46d3b422bac1b861431 languageName: node linkType: hard @@ -9030,7 +9030,7 @@ __metadata: "@babel/core": ~7.22.20 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/fuselage-tokens": ^0.33.1 "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/styled": ~0.31.25 @@ -9391,7 +9391,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.2 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.3 - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.32.0 @@ -10287,7 +10287,7 @@ __metadata: resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": ~7.22.20 - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/ui-contexts": "workspace:^" "@types/babel__core": ~7.20.3 "@types/react": ~17.0.69 @@ -10313,7 +10313,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ^0.36.0 "@rocket.chat/mock-providers": "workspace:^" @@ -10366,7 +10366,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/icons": ^0.36.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -10458,7 +10458,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ^0.36.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -10501,7 +10501,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ^0.36.0 "@rocket.chat/styled": ~0.31.25 @@ -10546,7 +10546,7 @@ __metadata: "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.54.3 + "@rocket.chat/fuselage": ^0.55.2 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.32.0 From 16a9c862d20d93c614ce76801f29d03f41b98abe Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:58:11 -0300 Subject: [PATCH 042/114] feat(apps): bridge method for removing users of a room (#32706) --- .changeset/hip-queens-taste.md | 5 ++ apps/meteor/app/apps/server/bridges/rooms.ts | 15 ++++-- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- ee/apps/ddp-streamer/package.json | 2 +- ee/packages/presence/package.json | 2 +- packages/apps/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/rest-typings/package.json | 2 +- yarn.lock | 48 ++++++-------------- 12 files changed, 38 insertions(+), 48 deletions(-) create mode 100644 .changeset/hip-queens-taste.md diff --git a/.changeset/hip-queens-taste.md b/.changeset/hip-queens-taste.md new file mode 100644 index 0000000000000..f1d7bb6f3f0e4 --- /dev/null +++ b/.changeset/hip-queens-taste.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Added the possibility for apps to remove users from a room diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index ae2121c3ecc28..86817c5721e2b 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -11,6 +11,7 @@ import { createDirectMessage } from '../../../../server/methods/createDirectMess import { createDiscussion } from '../../../discussion/server/methods/createDiscussion'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; +import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom'; import { createChannelMethod } from '../../../lib/server/methods/createChannel'; import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; @@ -210,6 +211,16 @@ export class AppRoomBridge extends RoomBridge { return users.map((user: ICoreUser) => userConverter.convertToApp(user)); } + protected async removeUsers(roomId: string, usernames: Array, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is removing users ${usernames} from room id: ${roomId}`); + if (!roomId) { + throw new Error('roomId was not provided.'); + } + + const members = await Users.findUsersByUsernames(usernames, { limit: 50 }).toArray(); + await Promise.all(members.map((user) => removeUserFromRoom(roomId, user))); + } + protected getMessages( _roomId: string, _options: { limit: number; skip?: number; sort?: Record }, @@ -217,8 +228,4 @@ export class AppRoomBridge extends RoomBridge { ): Promise { throw new Error('Method not implemented.'); } - - protected removeUsers(_roomId: string, _usernames: Array, _appId: string): Promise { - throw new Error('Method not implemented.'); - } } diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 45d1a5382af3f..b36f89a01c3ec 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -18,7 +18,7 @@ "author": "Rocket.Chat", "license": "MIT", "dependencies": { - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 17eef6e29f64d..0a4877dbeede6 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -231,7 +231,7 @@ "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", "@rocket.chat/apps": "workspace:^", - "@rocket.chat/apps-engine": "1.44.0-alpha.818", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/base64": "workspace:^", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-services": "workspace:^", diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 32facf54ba394..b20066036c207 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -15,7 +15,7 @@ ], "author": "Rocket.Chat", "dependencies": { - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 08d19ab91b588..0ff069768423f 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@babel/preset-env": "~7.22.20", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@types/node": "^14.18.63", diff --git a/packages/apps/package.json b/packages/apps/package.json index 04805db1c047c..3387deeea23ae 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -22,7 +22,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/model-typings": "workspace:^" } diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 7fbd33b3bce26..4b7ce783ac8c3 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -34,7 +34,7 @@ "extends": "../../package.json" }, "dependencies": { - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/message-parser": "workspace:^", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 08963a37b0ef1..7932b00b7e5f7 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -22,7 +22,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~" diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index ee3dd4e42da7d..622741719481d 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -63,7 +63,7 @@ "@babel/preset-env": "~7.22.20", "@babel/preset-react": "~7.22.15", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.55.2", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 0a612f9ee7dc5..9be27e7634cbc 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -24,7 +24,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/apps-engine": "1.43.0", + "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", diff --git a/yarn.lock b/yarn.lock index 938f9822053bb..3b68dcde38064 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8508,9 +8508,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/apps-engine@npm:1.43.0": - version: 1.43.0 - resolution: "@rocket.chat/apps-engine@npm:1.43.0" +"@rocket.chat/apps-engine@npm:alpha": + version: 1.44.0-alpha.814 + resolution: "@rocket.chat/apps-engine@npm:1.44.0-alpha.814" dependencies: "@msgpack/msgpack": 3.0.0-beta2 adm-zip: ^0.5.9 @@ -8526,29 +8526,7 @@ __metadata: uuid: ~8.3.2 peerDependencies: "@rocket.chat/ui-kit": "*" - checksum: d2a4be96fd56bd7790459cbe82d87601ce8cb4e846a8cd5d1cddbc699ff776f3dcd07b3b22a7e74f12e2f1b0361b6c3cf4a415254f1f5d75d537f5c5730ce05e - languageName: node - linkType: hard - -"@rocket.chat/apps-engine@npm:1.44.0-alpha.818": - version: 1.44.0-alpha.818 - resolution: "@rocket.chat/apps-engine@npm:1.44.0-alpha.818" - dependencies: - "@msgpack/msgpack": 3.0.0-beta2 - adm-zip: ^0.5.9 - cryptiles: ^4.1.3 - debug: ^4.3.4 - deno-bin: 1.37.1 - esbuild: ^0.20.2 - jose: ^4.11.1 - jsonrpc-lite: ^2.2.0 - lodash.clonedeep: ^4.5.0 - semver: ^5.7.1 - stack-trace: 0.0.10 - uuid: ~8.3.2 - peerDependencies: - "@rocket.chat/ui-kit": "*" - checksum: acef47bc7f13e0682d018531638b5168c3acd59beae37b013e881ea6fadfe12670fe10545f4a89487f7bedbe9166028833cba7ed3fc401d4283327e47e00e61c + checksum: 03f777ecd035af20c88558a941db77bb104f4402f78169a6d23e1629613c11ac23c04f6bdfb451a4273558789de58d4567661a7bd5de91b78464f98698e5f6a6 languageName: node linkType: hard @@ -8556,7 +8534,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/apps@workspace:packages/apps" dependencies: - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@types/jest": ~29.5.7 @@ -8635,7 +8613,7 @@ __metadata: "@babel/core": ~7.22.20 "@babel/preset-env": ~7.22.20 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/icons": ^0.36.0 @@ -8661,7 +8639,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/core-typings@workspace:packages/core-typings" dependencies: - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/icons": ^0.36.0 "@rocket.chat/message-parser": "workspace:^" @@ -8738,7 +8716,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer" dependencies: - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 @@ -8934,7 +8912,7 @@ __metadata: "@babel/preset-env": ~7.22.20 "@babel/preset-react": ~7.22.15 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": ^0.55.2 @@ -9379,7 +9357,7 @@ __metadata: "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" "@rocket.chat/apps": "workspace:^" - "@rocket.chat/apps-engine": 1.44.0-alpha.818 + "@rocket.chat/apps-engine": alpha "@rocket.chat/base64": "workspace:^" "@rocket.chat/cas-validate": "workspace:^" "@rocket.chat/core-services": "workspace:^" @@ -10017,7 +9995,7 @@ __metadata: "@babel/core": ~7.22.20 "@babel/preset-env": ~7.22.20 "@babel/preset-typescript": ~7.22.15 - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" @@ -10132,7 +10110,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/rest-typings@workspace:packages/rest-typings" dependencies: - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/message-parser": "workspace:^" @@ -37182,7 +37160,7 @@ __metadata: version: 0.0.0-use.local resolution: "rocketchat-services@workspace:apps/meteor/ee/server/services" dependencies: - "@rocket.chat/apps-engine": 1.43.0 + "@rocket.chat/apps-engine": alpha "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": ~0.31.25 From 3b4b19cfc53c068f360b83ce3d291308a104a1fe Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 15 Jul 2024 14:32:50 -0300 Subject: [PATCH 043/114] feat: add accounts setting to reorganize sections (#32744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Júlia Jaeger Foresti <60678893+juliajforesti@users.noreply.github.com> --- .changeset/selfish-emus-sing.md | 6 +++ .../client/sidebar/hooks/useRoomList.ts | 41 ++++++++++++++++++- apps/meteor/server/settings/accounts.ts | 22 ++++++++++ .../tests/end-to-end/api/miscellaneous.ts | 1 + packages/i18n/src/locales/en.i18n.json | 11 +++-- 5 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 .changeset/selfish-emus-sing.md diff --git a/.changeset/selfish-emus-sing.md b/.changeset/selfish-emus-sing.md new file mode 100644 index 0000000000000..315d674a1857c --- /dev/null +++ b/.changeset/selfish-emus-sing.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Added account setting `Accounts_Default_User_Preferences_sidebarSectionsOrder` to allow users to reorganize sidebar sections diff --git a/apps/meteor/client/sidebar/hooks/useRoomList.ts b/apps/meteor/client/sidebar/hooks/useRoomList.ts index fa5dfd2797cb1..afdc57086dc44 100644 --- a/apps/meteor/client/sidebar/hooks/useRoomList.ts +++ b/apps/meteor/client/sidebar/hooks/useRoomList.ts @@ -12,12 +12,39 @@ const query = { open: { $ne: false } }; const emptyQueue: ILivechatInquiryRecord[] = []; +const order: ( + | 'Incoming_Calls' + | 'Incoming_Livechats' + | 'Open_Livechats' + | 'On_Hold_Chats' + | 'Unread' + | 'Favorites' + | 'Teams' + | 'Discussions' + | 'Channels' + | 'Direct_Messages' + | 'Conversations' +)[] = [ + 'Incoming_Calls', + 'Incoming_Livechats', + 'Open_Livechats', + 'On_Hold_Chats', + 'Unread', + 'Favorites', + 'Teams', + 'Discussions', + 'Channels', + 'Direct_Messages', + 'Conversations', +]; + export const useRoomList = (): Array => { const [roomList, setRoomList] = useDebouncedState<(ISubscription & IRoom)[]>([], 150); const showOmnichannel = useOmnichannelEnabled(); const sidebarGroupByType = useUserPreference('sidebarGroupByType'); const favoritesEnabled = useUserPreference('sidebarShowFavorites'); + const sidebarOrder = useUserPreference('sidebarSectionsOrder') ?? order; const isDiscussionEnabled = useSetting('Discussion_enabled'); const sidebarShowUnread = useUserPreference('sidebarShowUnread'); @@ -92,7 +119,7 @@ export const useRoomList = (): Array => { }); const groups = new Map(); - incomingCall.size && groups.set('Incoming Calls', incomingCall); + incomingCall.size && groups.set('Incoming_Calls', incomingCall); showOmnichannel && inquiries.enabled && queue.length && groups.set('Incoming_Livechats', queue); showOmnichannel && omnichannel.size && groups.set('Open_Livechats', omnichannel); showOmnichannel && onHold.size && groups.set('On_Hold_Chats', onHold); @@ -103,7 +130,16 @@ export const useRoomList = (): Array => { sidebarGroupByType && channels.size && groups.set('Channels', channels); sidebarGroupByType && direct.size && groups.set('Direct_Messages', direct); !sidebarGroupByType && groups.set('Conversations', conversation); - return [...groups.entries()].flatMap(([key, group]) => [key, ...group]); + return sidebarOrder + .map((key) => { + const group = groups.get(key); + if (!group) { + return []; + } + + return [key, ...group]; + }) + .flat(); }); }, [ rooms, @@ -116,6 +152,7 @@ export const useRoomList = (): Array => { sidebarGroupByType, setRoomList, isDiscussionEnabled, + sidebarOrder, ]); return roomList; diff --git a/apps/meteor/server/settings/accounts.ts b/apps/meteor/server/settings/accounts.ts index 2b1dac892c07c..39e4183dbf5f1 100644 --- a/apps/meteor/server/settings/accounts.ts +++ b/apps/meteor/server/settings/accounts.ts @@ -709,6 +709,28 @@ export const createAccountSettings = () => public: true, i18nLabel: 'VideoConf_Mobile_Ringing', }); + + const defaultUserPreferencesSidebarSectionsOrder = [ + 'Incoming_Calls', + 'Incoming_Livechats', + 'Open_Livechats', + 'On_Hold_Chats', + 'Unread', + 'Favorites', + 'Teams', + 'Discussions', + 'Channels', + 'Direct_Messages', + 'Conversations', + ]; + + await this.add('Accounts_Default_User_Preferences_sidebarSectionsOrder', defaultUserPreferencesSidebarSectionsOrder, { + type: 'multiSelect', + public: true, + values: defaultUserPreferencesSidebarSectionsOrder.map((key) => ({ key, i18nLabel: key })), + i18nLabel: 'Sidebar_Sections_Order', + i18nDescription: 'Sidebar_Sections_Order_Description', + }); }); await this.section('Avatar', async function () { diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index 6469ef051a245..613a874ecd8c5 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -182,6 +182,7 @@ describe('miscellaneous', () => { 'sidebarViewMode', 'sidebarDisplayAvatar', 'sidebarGroupByType', + 'sidebarSectionsOrder', 'muteFocusedConversations', 'notifyCalendarEvents', 'enableMobileRinging', diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index e168ec9cf7df1..875996b538283 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2699,7 +2699,7 @@ "Inclusive": "Inclusive", "Incoming": "Incoming", "Incoming_call_from": "Incoming call from", - "Incoming_Livechats": "Queued Chats", + "Incoming_Livechats": "Queued chats", "Incoming_WebHook": "Incoming WebHook", "Industry": "Industry", "Info": "Info", @@ -4016,7 +4016,7 @@ "on-hold-others-livechat-room": "On Hold Others Omnichannel Room", "on-hold-others-livechat-room_description": "Permission to on hold others omnichannel room", "On_Hold": "On hold", - "On_Hold_Chats": "On Hold", + "On_Hold_Chats": "On hold", "On_Hold_conversations": "On hold conversations", "online": "online", "Online": "Online", @@ -4040,7 +4040,7 @@ "Open_days_of_the_week": "Open Days of the Week", "Open_Dialpad": "Open Dialpad", "Open_directory": "Open directory", - "Open_Livechats": "Chats in Progress", + "Open_Livechats": "Chats in progress", "Open_Outlook": "Open Outlook", "Open_settings": "Open settings", "Open_sidebar": "Open sidebar", @@ -6480,5 +6480,8 @@ "You_cant_take_chats_offline": "You cannot take new conversations because you're offline", "New_navigation": "Enhanced navigation experience", "New_navigation_description": "Explore our improved navigation, designed with clear scopes for easy access to what you need. This change serves as the foundation for future advancements in navigation management.", - "Workspace_and_user_settings": "Workspace and user settings" + "Workspace_and_user_settings": "Workspace and user settings", + "Sidebar_Sections_Order": "Sidebar sections order", + "Sidebar_Sections_Order_Description": "Select the categories in your preferred order", + "Incoming_Calls": "Incoming calls" } From 48ea8fce837d3059300ed8f399130cd58da305bb Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Mon, 15 Jul 2024 16:29:24 -0300 Subject: [PATCH 044/114] fix: AAC audio type not playing on web (#32763) --- .../app/api/server/lib/getUploadFormData.ts | 4 +- apps/meteor/app/utils/lib/mimeTypes.spec.ts | 89 +++++++++++++++++++ apps/meteor/app/utils/lib/mimeTypes.ts | 13 ++- .../body/hooks/useFileUploadDropTarget.ts | 2 +- .../hooks/useFileUploadAction.ts | 2 +- apps/meteor/jest.config.ts | 1 + 6 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 apps/meteor/app/utils/lib/mimeTypes.spec.ts diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 85fc0658542d4..3136a6c16e130 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -63,7 +63,7 @@ export async function getUploadFormData< function onFile( fieldname: string, file: Readable & { truncated: boolean }, - { filename, encoding }: { filename: string; encoding: string }, + { filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string }, ) { if (options.field && fieldname !== options.field) { file.resume(); @@ -85,7 +85,7 @@ export async function getUploadFormData< file, filename, encoding, - mimetype: getMimeType(filename), + mimetype: getMimeType(mimetype, filename), fieldname, fields, fileBuffer: Buffer.concat(fileChunks), diff --git a/apps/meteor/app/utils/lib/mimeTypes.spec.ts b/apps/meteor/app/utils/lib/mimeTypes.spec.ts new file mode 100644 index 0000000000000..d0fbd4360e24a --- /dev/null +++ b/apps/meteor/app/utils/lib/mimeTypes.spec.ts @@ -0,0 +1,89 @@ +import { expect } from 'chai'; + +import { getExtension, getMimeType } from './mimeTypes'; + +const mimeTypeToExtension = { + 'text/plain': 'txt', + 'image/x-icon': 'ico', + 'image/vnd.microsoft.icon': 'ico', + 'image/png': 'png', + 'image/jpeg': 'jpeg', + 'image/gif': 'gif', + 'image/webp': 'webp', + 'image/svg+xml': 'svg', + 'image/bmp': 'bmp', + 'image/tiff': 'tif', + 'audio/wav': 'wav', + 'audio/wave': 'wav', + 'audio/aac': 'aac', + 'audio/x-aac': 'aac', + 'audio/mp4': 'm4a', + 'audio/mpeg': 'mpga', + 'audio/ogg': 'oga', + 'application/octet-stream': 'bin', +}; + +const extensionToMimeType = { + lst: 'text/plain', + txt: 'text/plain', + ico: 'image/x-icon', + png: 'image/png', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + bmp: 'image/bmp', + tiff: 'image/tiff', + tif: 'image/tiff', + wav: 'audio/wav', + aac: 'audio/aac', + mp3: 'audio/mpeg', + ogg: 'audio/ogg', + oga: 'audio/ogg', + m4a: 'audio/mp4', + mpga: 'audio/mpeg', + mp4: 'video/mp4', + bin: 'application/octet-stream', +}; + +describe('mimeTypes', () => { + describe('getExtension', () => { + for (const [mimeType, extension] of Object.entries(mimeTypeToExtension)) { + it(`should return the correct extension ${extension} for the given mimeType ${mimeType}`, async () => { + expect(getExtension(mimeType)).to.be.eql(extension); + }); + } + + it('should return an empty string if the mimeType is not found', async () => { + expect(getExtension('application/unknown')).to.be.eql(''); + }); + }); + + describe('getMimeType', () => { + for (const [extension, mimeType] of Object.entries(extensionToMimeType)) { + it(`should return the correct mimeType ${mimeType} for the given fileName file.${extension} passing the correct mimeType`, async () => { + expect(getMimeType(mimeType, `file.${extension}`)).to.be.eql(mimeType); + }); + } + + it('should return the correct mimeType for the given fileName', async () => { + for (const [extension, mimeType] of Object.entries(extensionToMimeType)) { + expect(getMimeType('application/unknown', `file.${extension}`)).to.be.eql(mimeType); + } + }); + + it('should return the correct mimeType for the given fileName when informed mimeType is application/octet-stream', async () => { + for (const [extension, mimeType] of Object.entries(extensionToMimeType)) { + expect(getMimeType('application/octet-stream', `file.${extension}`)).to.be.eql(mimeType); + } + }); + + it('should return the mimeType if it is not application/octet-stream', async () => { + expect(getMimeType('audio/wav', 'file.wav')).to.be.eql('audio/wav'); + }); + + it('should return application/octet-stream if the mimeType is not found', async () => { + expect(getMimeType('application/octet-stream', 'file.unknown')).to.be.eql('application/octet-stream'); + }); + }); +}); diff --git a/apps/meteor/app/utils/lib/mimeTypes.ts b/apps/meteor/app/utils/lib/mimeTypes.ts index 909a955d6724d..df670145b494f 100644 --- a/apps/meteor/app/utils/lib/mimeTypes.ts +++ b/apps/meteor/app/utils/lib/mimeTypes.ts @@ -3,8 +3,8 @@ import mime from 'mime-type/with-db'; mime.types.wav = 'audio/wav'; mime.types.lst = 'text/plain'; mime.define('image/vnd.microsoft.icon', { source: '', extensions: ['ico'] }, mime.dupAppend); -mime.define('image/x-icon', { source: '', extensions: ['ico'] }, mime.dupAppend); -mime.types.ico = 'image/x-icon'; +mime.define('image/x-icon', { source: '', extensions: ['ico'] }, mime.dupOverwrite); +mime.define('audio/aac', { source: '', extensions: ['aac'] }, mime.dupOverwrite); const getExtension = (param: string): string => { const extension = mime.extension(param); @@ -12,7 +12,14 @@ const getExtension = (param: string): string => { return !extension || typeof extension === 'boolean' ? '' : extension; }; -const getMimeType = (fileName: string): string => { +const getMimeType = (mimetype: string, fileName: string): string => { + // If the extension from the mimetype is different from the file extension, the file + // extension may be wrong so use the informed mimetype + const extension = mime.extension(mimetype); + if (mimetype !== 'application/octet-stream' && extension && extension !== fileName.split('.').pop()) { + return mimetype; + } + const fileMimeType = mime.lookup(fileName); return typeof fileMimeType === 'string' ? fileMimeType : 'application/octet-stream'; }; diff --git a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts index 414b91c52493b..314eb64304b59 100644 --- a/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts +++ b/apps/meteor/client/views/room/body/hooks/useFileUploadDropTarget.ts @@ -54,7 +54,7 @@ export const useFileUploadDropTarget = (): readonly [ const uniqueFiles = getUniqueFiles(); const uploads = Array.from(uniqueFiles).map((file) => { - Object.defineProperty(file, 'type', { value: getMimeType(file.name) }); + Object.defineProperty(file, 'type', { value: getMimeType(file.type, file.name) }); return file; }); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index 03229c5dceb3b..f911b2b63b1f4 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -26,7 +26,7 @@ export const useFileUploadAction = (disabled: boolean): GenericMenuItemProps => const { getMimeType } = await import('../../../../../../../app/utils/lib/mimeTypes'); const filesToUpload = Array.from(fileInputRef?.current?.files ?? []).map((file) => { Object.defineProperty(file, 'type', { - value: getMimeType(file.name), + value: getMimeType(file.type, file.name), }); return file; }); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index 72538cf14d161..fb9c6f1247a93 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -34,6 +34,7 @@ const config: Config = { '/app/livechat/server/api/**/*.spec.ts', '/ee/app/authorization/server/validateUserRoles.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', + '/app/utils/lib/**.spec.ts', ], transformIgnorePatterns: ['!/node_modules/jose'], errorOnDeprecated: true, From 25da5280a5d99b73bbae322b52a59dc4f6d532f3 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 15 Jul 2024 22:42:39 -0300 Subject: [PATCH 045/114] fix: Wrong federation setting translation value (#32788) --- .changeset/grumpy-worms-appear.md | 5 +++++ packages/i18n/src/locales/en.i18n.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/grumpy-worms-appear.md diff --git a/.changeset/grumpy-worms-appear.md b/.changeset/grumpy-worms-appear.md new file mode 100644 index 0000000000000..fb9fab77b24c1 --- /dev/null +++ b/.changeset/grumpy-worms-appear.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/i18n": patch +--- + +Fixed wrong wording on a federation setting diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 875996b538283..f9fcf0163cafb 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2320,8 +2320,8 @@ "Federation_Matrix_not_allowed_to_change_moderator": "You are not allowed to change the moderator", "Federation_Matrix_not_allowed_to_change_owner": "You are not allowed to change the owner", "Federation_Matrix_join_public_rooms_is_premium": "Join federated rooms is a Premium feature", - "Federation_Matrix_max_size_of_public_rooms_users": "Maximum number of users when joining a public room in a remote server", - "Federation_Matrix_max_size_of_public_rooms_users_desc": "The number of the maximum users when joining a public room in a remote server. Public Rooms with more users will be ignored in the list of Public Rooms to join.", + "Federation_Matrix_max_size_of_public_rooms_users": "Maximum number of members when joining a public room in a remote server", + "Federation_Matrix_max_size_of_public_rooms_users_desc": "The user limit from a public room in a remote server that can still be joined. Rooms that exceed this setting will still be listed, but users won't be able to join them", "Federation_Matrix_max_size_of_public_rooms_users_Alert": "Keep in mind, that the bigger the room you allow for users to join, the more time it will take to join that room, besides the amount of resource it will use. Read more", "Federation_Matrix_serve_well_known": "Serve Well Known", "Federation_Matrix_serve_well_known_Description": "Serve /.well-known/matrix/server and /.well-known/matrix/client directly from within Rocket.Chat instead of reverse proxy for federation", From d0bedf7bb14446c1f22c47b60ad96ddd62d9e85f Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 16 Jul 2024 00:07:51 -0300 Subject: [PATCH 046/114] feat: replicate IPreRoomUserLeave and IPostRoomUserLeave event in meteor method and add removedBy to IRoomUserLeaveContext (#32724) --- .changeset/sour-forks-breathe.md | 5 +++++ apps/meteor/app/apps/server/bridges/listeners.js | 3 ++- .../app/lib/server/functions/removeUserFromRoom.ts | 10 +++------- apps/meteor/server/methods/removeUserFromRoom.ts | 14 ++++++++++++++ apps/meteor/server/services/room/service.ts | 2 +- apps/meteor/server/services/team/service.ts | 4 ++-- 6 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .changeset/sour-forks-breathe.md diff --git a/.changeset/sour-forks-breathe.md b/.changeset/sour-forks-breathe.md new file mode 100644 index 0000000000000..2d1076845fa90 --- /dev/null +++ b/.changeset/sour-forks-breathe.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Extended apps-engine events for users leaving a room to also fire when being removed by another user. Also added the triggering user's information to the event's context payload. diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index ab2632c912b0d..13db1179310c5 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -143,10 +143,11 @@ export class AppListenerBridge { }; case AppInterface.IPreRoomUserLeave: case AppInterface.IPostRoomUserLeave: - const [leavingUser] = payload; + const [leavingUser, removedBy] = payload; return { room: rm, leavingUser: this.orch.getConverters().get('users').convertToApp(leavingUser), + removedBy: this.orch.getConverters().get('users').convertToApp(removedBy), }; default: return rm; diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index 3b065c68f15c4..c55ee382f10cf 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -10,11 +10,7 @@ import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRo import { settings } from '../../../settings/server'; import { notifyOnRoomChangedById } from '../lib/notifyListener'; -export const removeUserFromRoom = async function ( - rid: string, - user: IUser, - options?: { byUser: Pick }, -): Promise { +export const removeUserFromRoom = async function (rid: string, user: IUser, options?: { byUser: IUser }): Promise { const room = await Rooms.findOneById(rid); if (!room) { @@ -22,7 +18,7 @@ export const removeUserFromRoom = async function ( } try { - await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user); + await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user, options?.byUser); } catch (error: any) { if (error.name === AppsEngineException.name) { throw new Meteor.Error('error-app-prevented', error.message); @@ -75,5 +71,5 @@ export const removeUserFromRoom = async function ( void notifyOnRoomChangedById(rid); - await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user); + await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user, options?.byUser); }; diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index 08cd6c5b25e03..6662ae8d22cfa 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -1,3 +1,5 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; +import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Message, Team } from '@rocket.chat/core-services'; import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; @@ -75,6 +77,16 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri } } + try { + await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, removedUser, fromUser); + } catch (error: any) { + if (error.name === AppsEngineException.name) { + throw new Meteor.Error('error-app-prevented', error.message); + } + + throw error; + } + await callbacks.run('beforeRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room); await Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id); @@ -99,6 +111,8 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri void notifyOnRoomChanged(room); }); + await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, removedUser, fromUser); + return true; }; diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index b112209b681b2..4b0fe2d177b59 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -66,7 +66,7 @@ export class RoomService extends ServiceClassInternal implements IRoomService { return addUserToRoom(roomId, user, inviter, silenced); } - async removeUserFromRoom(roomId: string, user: IUser, options?: { byUser: Pick }): Promise { + async removeUserFromRoom(roomId: string, user: IUser, options?: { byUser: IUser }): Promise { return removeUserFromRoom(roomId, user, options); } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index f81b21d7fa013..be153d565f182 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -771,7 +771,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { const usersToRemove = await Users.findByIds(membersIds, { projection: { _id: 1, username: 1 }, }).toArray(); - const byUser = (await Users.findOneById(uid, { projection: { _id: 1, username: 1 } })) as Pick; + const byUser = await Users.findOneById(uid); for await (const member of members) { if (!member.userId) { @@ -802,7 +802,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { await removeUserFromRoom( team.roomId, removedUser, - uid !== member.userId + uid !== member.userId && byUser ? { byUser, } From eff91b44a5eeaf3d84065d8231831e7549a315ee Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 16 Jul 2024 12:26:51 -0600 Subject: [PATCH 047/114] test: Flaky `mention @all @here` yet again (#32799) --- apps/meteor/tests/e2e/message-mentions.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/message-mentions.spec.ts b/apps/meteor/tests/e2e/message-mentions.spec.ts index 9d5cd7e622cef..15abd9c36b400 100644 --- a/apps/meteor/tests/e2e/message-mentions.spec.ts +++ b/apps/meteor/tests/e2e/message-mentions.spec.ts @@ -56,7 +56,7 @@ test.describe.serial('Should not allow to send @all mention if permission to do await expect(page).toHaveURL(`/group/${targetChannel2}`); }); await test.step('receive notify message', async () => { - await adminPage.content.dispatchSlashCommand('@all'); + await adminPage.content.sendMessage('@all '); await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed'); }); }); @@ -98,7 +98,7 @@ test.describe.serial('Should not allow to send @here mention if permission to do await expect(page).toHaveURL(`/group/${targetChannel2}`); }); await test.step('receive notify message', async () => { - await adminPage.content.dispatchSlashCommand('@here'); + await adminPage.content.sendMessage('@here '); await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed'); }); }); From 8fc6ca8b4e09b38ea9c74dbed586b8bc3694cf09 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 16 Jul 2024 13:19:07 -0600 Subject: [PATCH 048/114] fix: `Accounts_LoginExpiration` being used differently on codebase (#32527) --- .changeset/empty-readers-teach.md | 8 +++++ apps/meteor/app/api/server/v1/users.ts | 6 ++-- .../authentication/server/startup/index.js | 3 +- ee/apps/account-service/Dockerfile | 3 ++ ee/apps/account-service/package.json | 1 + ee/apps/account-service/src/Account.ts | 11 +++--- ee/apps/account-service/src/lib/utils.ts | 3 +- packages/tools/jest.config.ts | 3 ++ packages/tools/package.json | 2 ++ packages/tools/src/converter.spec.ts | 15 ++++++++ packages/tools/src/converter.ts | 7 ++++ packages/tools/src/getLoginExpiration.spec.ts | 35 +++++++++++++++++++ packages/tools/src/getLoginExpiration.ts | 16 +++++++++ packages/tools/src/index.ts | 2 ++ yarn.lock | 1 + 15 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 .changeset/empty-readers-teach.md create mode 100644 packages/tools/jest.config.ts create mode 100644 packages/tools/src/converter.spec.ts create mode 100644 packages/tools/src/converter.ts create mode 100644 packages/tools/src/getLoginExpiration.spec.ts create mode 100644 packages/tools/src/getLoginExpiration.ts diff --git a/.changeset/empty-readers-teach.md b/.changeset/empty-readers-teach.md new file mode 100644 index 0000000000000..b4bd075ef654c --- /dev/null +++ b/.changeset/empty-readers-teach.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/tools": patch +"@rocket.chat/account-service": patch +--- + +Fixed an inconsistent evaluation of the `Accounts_LoginExpiration` setting over the codebase. In some places, it was being used as milliseconds while in others as days. Invalid values produced different results. A helper function was created to centralize the setting validation and the proper value being returned to avoid edge cases. +Negative values may be saved on the settings UI panel but the code will interpret any negative, NaN or 0 value to the default expiration which is 90 days. diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index c26957fa19910..041ef0410df0a 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -19,6 +19,7 @@ import { isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, } from '@rocket.chat/rest-typings'; +import { getLoginExpirationInMs } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -1048,8 +1049,9 @@ API.v1.addRoute( const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken); - const tokenExpires = - (token && 'when' in token && new Date(token.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000)) || undefined; + const loginExp = settings.get('Accounts_LoginExpiration'); + + const tokenExpires = (token && 'when' in token && new Date(token.when.getTime() + getLoginExpirationInMs(loginExp))) || undefined; return API.v1.success({ token: xAuthToken, diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index bffbe1f9876dd..2e4c599ce558c 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -2,6 +2,7 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { User } from '@rocket.chat/core-services'; import { Roles, Settings, Users } from '@rocket.chat/models'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; +import { getLoginExpirationInDays } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -31,7 +32,7 @@ Accounts.config({ Meteor.startup(() => { settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => { - Accounts._options.loginExpirationInDays = settings.get('Accounts_LoginExpiration'); + Accounts._options.loginExpirationInDays = getLoginExpirationInDays(settings.get('Accounts_LoginExpiration')); Accounts.emailTemplates.siteName = settings.get('Site_Name'); diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index c662d87653003..acbc5b0371d2e 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -43,6 +43,9 @@ COPY ./ee/packages/license/dist packages/license/dist COPY ./packages/ui-kit/package.json packages/ui-kit/package.json COPY ./packages/ui-kit/dist packages/ui-kit/dist +COPY ./packages/tools/package.json packages/tools/package.json +COPY ./packages/tools/dist packages/tools/dist + COPY ./ee/apps/${SERVICE}/dist . COPY ./package.json . diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index c41932159e0e0..194a3e1760e50 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -22,6 +22,7 @@ "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", + "@rocket.chat/tools": "workspace:^", "@types/node": "^14.18.63", "bcrypt": "^5.0.1", "ejson": "^2.2.3", diff --git a/ee/apps/account-service/src/Account.ts b/ee/apps/account-service/src/Account.ts index 4521574c12c7e..c71376d301f3e 100644 --- a/ee/apps/account-service/src/Account.ts +++ b/ee/apps/account-service/src/Account.ts @@ -1,6 +1,7 @@ import { ServiceClass } from '@rocket.chat/core-services'; import type { IAccount, ILoginResult } from '@rocket.chat/core-services'; import { Settings } from '@rocket.chat/models'; +import { getLoginExpirationInDays } from '@rocket.chat/tools'; import { loginViaResume } from './lib/loginViaResume'; import { loginViaUsername } from './lib/loginViaUsername'; @@ -22,9 +23,8 @@ export class Account extends ServiceClass implements IAccount { if (_id !== 'Accounts_LoginExpiration') { return; } - if (typeof value === 'number') { - this.loginExpiration = value; - } + + this.loginExpiration = getLoginExpirationInDays(value as number); }); } @@ -46,8 +46,7 @@ export class Account extends ServiceClass implements IAccount { async started(): Promise { const expiry = await Settings.findOne({ _id: 'Accounts_LoginExpiration' }, { projection: { value: 1 } }); - if (expiry?.value) { - this.loginExpiration = expiry.value as number; - } + + this.loginExpiration = getLoginExpirationInDays(expiry?.value as number); } } diff --git a/ee/apps/account-service/src/lib/utils.ts b/ee/apps/account-service/src/lib/utils.ts index 1d4a3a6ad5805..f52f323606de7 100644 --- a/ee/apps/account-service/src/lib/utils.ts +++ b/ee/apps/account-service/src/lib/utils.ts @@ -1,5 +1,6 @@ import crypto from 'crypto'; +import { convertFromDaysToMilliseconds } from '@rocket.chat/tools'; import bcrypt from 'bcrypt'; import { v4 as uuidv4 } from 'uuid'; @@ -60,4 +61,4 @@ export const validatePassword = (password: string, bcryptPassword: string): Prom bcrypt.compare(getPassword(password), bcryptPassword); export const _tokenExpiration = (when: string | Date, expirationInDays: number): Date => - new Date(new Date(when).getTime() + expirationInDays * 60 * 60 * 24 * 1000); + new Date(new Date(when).getTime() + convertFromDaysToMilliseconds(expirationInDays)); diff --git a/packages/tools/jest.config.ts b/packages/tools/jest.config.ts new file mode 100644 index 0000000000000..959a31a7c6bfc --- /dev/null +++ b/packages/tools/jest.config.ts @@ -0,0 +1,3 @@ +export default { + preset: 'ts-jest', +}; diff --git a/packages/tools/package.json b/packages/tools/package.json index e6bbae9b5be6e..ac79955314b8c 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -13,7 +13,9 @@ "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "test": "jest", + "test:cov": "jest --coverage", "build": "rm -rf dist && tsc -p tsconfig.json", + "testunit": "jest", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, "main": "./dist/index.js", diff --git a/packages/tools/src/converter.spec.ts b/packages/tools/src/converter.spec.ts new file mode 100644 index 0000000000000..5a27b6a97b95a --- /dev/null +++ b/packages/tools/src/converter.spec.ts @@ -0,0 +1,15 @@ +import { convertFromDaysToMilliseconds } from './converter'; + +describe('convertFromDaysToMilliseconds', () => { + it('should throw an error when a non number is passed', () => { + // @ts-expect-error - Testing + expect(() => convertFromDaysToMilliseconds('90')).toThrow(); + }); + it('should return the value passed when its valid', () => { + expect(convertFromDaysToMilliseconds(85)).toBe(85 * 24 * 60 * 60 * 1000); + }); + it('should fail if anything but an integer is passed', () => { + expect(() => convertFromDaysToMilliseconds(85.5)).toThrow(); + expect(() => convertFromDaysToMilliseconds(-2.3)).toThrow(); + }); +}); diff --git a/packages/tools/src/converter.ts b/packages/tools/src/converter.ts new file mode 100644 index 0000000000000..e71c264857dcb --- /dev/null +++ b/packages/tools/src/converter.ts @@ -0,0 +1,7 @@ +export const convertFromDaysToMilliseconds = (days: number) => { + if (typeof days !== 'number' || !Number.isInteger(days)) { + throw new Error('days must be a number'); + } + + return days * 24 * 60 * 60 * 1000; +}; diff --git a/packages/tools/src/getLoginExpiration.spec.ts b/packages/tools/src/getLoginExpiration.spec.ts new file mode 100644 index 0000000000000..edd652172a5e4 --- /dev/null +++ b/packages/tools/src/getLoginExpiration.spec.ts @@ -0,0 +1,35 @@ +import { getLoginExpirationInDays, getLoginExpirationInMs } from './getLoginExpiration'; + +describe('getLoginExpirationInDays', () => { + it('should return 90 by default', () => { + expect(getLoginExpirationInDays()).toBe(90); + }); + it('should return 90 when value is 0', () => { + expect(getLoginExpirationInDays(0)).toBe(90); + }); + it('should return 90 when value is NaN', () => { + expect(getLoginExpirationInDays(NaN)).toBe(90); + }); + it('should return 90 when value is negative', () => { + expect(getLoginExpirationInDays(-1)).toBe(90); + }); + it('should return 90 when value is undefined', () => { + expect(getLoginExpirationInDays(undefined)).toBe(90); + }); + it('should return 90 when value is not a number', () => { + // @ts-expect-error - Testing + expect(getLoginExpirationInDays('90')).toBe(90); + }); + it('should return the value passed when its valid', () => { + expect(getLoginExpirationInDays(85)).toBe(85); + }); +}); + +describe('getLoginExpirationInMs', () => { + it('should return 90 days in milliseconds when no value is passed', () => { + expect(getLoginExpirationInMs()).toBe(90 * 24 * 60 * 60 * 1000); + }); + it('should return the value passed when its valid', () => { + expect(getLoginExpirationInMs(85)).toBe(85 * 24 * 60 * 60 * 1000); + }); +}); diff --git a/packages/tools/src/getLoginExpiration.ts b/packages/tools/src/getLoginExpiration.ts new file mode 100644 index 0000000000000..de21fdce21a87 --- /dev/null +++ b/packages/tools/src/getLoginExpiration.ts @@ -0,0 +1,16 @@ +import { convertFromDaysToMilliseconds } from './converter'; + +const ACCOUNTS_DEFAULT_LOGIN_EXPIRATION_DAYS = 90; + +// Given a value, validates if it mets the conditions to be a valid login expiration. +// Else, returns the default login expiration (which for Meteor is 90 days) +export const getLoginExpirationInDays = (expiry?: number) => { + if (expiry && typeof expiry === 'number' && !Number.isNaN(expiry) && expiry > 0) { + return expiry; + } + return ACCOUNTS_DEFAULT_LOGIN_EXPIRATION_DAYS; +}; + +export const getLoginExpirationInMs = (expiry?: number) => { + return convertFromDaysToMilliseconds(getLoginExpirationInDays(expiry)); +}; diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index b1b53ab71a90a..96faa4d55969e 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -4,3 +4,5 @@ export * from './pick'; export * from './stream'; export * from './timezone'; export * from './wrapExceptions'; +export * from './getLoginExpiration'; +export * from './converter'; diff --git a/yarn.lock b/yarn.lock index 3b68dcde38064..add9a5e19468d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8432,6 +8432,7 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 + "@rocket.chat/tools": "workspace:^" "@types/bcrypt": ^5.0.1 "@types/gc-stats": ^1.4.2 "@types/node": ^14.18.63 From fa82159492bb1589372ba4f7dcca716da7a8a74b Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:01:41 -0300 Subject: [PATCH 049/114] fix: Agents can't leave omnichannel rooms that have already been closed (#32707) --- .changeset/happy-peaches-nail.md | 5 + .../lib/server/functions/closeLivechatRoom.ts | 81 +++++++++ .../meteor/app/livechat/server/api/v1/room.ts | 49 +----- .../app/livechat/server/methods/closeRoom.ts | 15 +- .../QuickActions/hooks/useQuickActions.tsx | 4 +- .../functions/closeLivechatRoom.tests.ts | 154 ++++++++++++++++++ 6 files changed, 256 insertions(+), 52 deletions(-) create mode 100644 .changeset/happy-peaches-nail.md create mode 100644 apps/meteor/app/lib/server/functions/closeLivechatRoom.ts create mode 100644 apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts diff --git a/.changeset/happy-peaches-nail.md b/.changeset/happy-peaches-nail.md new file mode 100644 index 0000000000000..2dfb2151ced05 --- /dev/null +++ b/.changeset/happy-peaches-nail.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with livechat agents not being able to leave omnichannel rooms if joining after a room has been closed by the visitor (due to race conditions) diff --git a/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts new file mode 100644 index 0000000000000..b716be044d578 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/closeLivechatRoom.ts @@ -0,0 +1,81 @@ +import type { IUser, IRoom, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms, Subscriptions } from '@rocket.chat/models'; + +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped'; +import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; + +export const closeLivechatRoom = async ( + user: IUser, + roomId: IRoom['_id'], + { + comment, + tags, + generateTranscriptPdf, + transcriptEmail, + }: { + comment?: string; + tags?: string[]; + generateTranscriptPdf?: boolean; + transcriptEmail?: + | { + sendToVisitor: false; + } + | { + sendToVisitor: true; + requestData: Pick, 'email' | 'subject'>; + }; + }, +): Promise => { + const room = await LivechatRooms.findOneById(roomId); + if (!room || !isOmnichannelRoom(room)) { + throw new Error('error-invalid-room'); + } + + if (!room.open) { + const subscriptionsLeft = await Subscriptions.countByRoomId(roomId); + if (subscriptionsLeft) { + await Subscriptions.removeByRoomId(roomId); + return; + } + throw new Error('error-room-already-closed'); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { projection: { _id: 1 } }); + if (!subscription && !(await hasPermissionAsync(user._id, 'close-others-livechat-room'))) { + throw new Error('error-not-authorized'); + } + + const options: CloseRoomParams['options'] = { + clientAction: true, + tags, + ...(generateTranscriptPdf && { pdfTranscript: { requestedBy: user._id } }), + ...(transcriptEmail && { + ...(transcriptEmail.sendToVisitor + ? { + emailTranscript: { + sendToVisitor: true, + requestData: { + email: transcriptEmail.requestData.email, + subject: transcriptEmail.requestData.subject, + requestedAt: new Date(), + requestedBy: user, + }, + }, + } + : { + emailTranscript: { + sendToVisitor: false, + }, + }), + }), + }; + + await Livechat.closeRoom({ + room, + user, + options, + comment, + }); +}; diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 8a663fb0bd6d6..94674f801ad5e 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -1,7 +1,7 @@ import { Omnichannel } from '@rocket.chat/core-services'; import type { ILivechatAgent, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { LivechatVisitors, Users, LivechatRooms, Subscriptions, Messages } from '@rocket.chat/models'; +import { LivechatVisitors, Users, LivechatRooms, Messages } from '@rocket.chat/models'; import { isLiveChatRoomForwardProps, isPOSTLivechatRoomCloseParams, @@ -21,6 +21,7 @@ import { isWidget } from '../../../../api/server/helpers/isWidget'; import { canAccessRoomAsync, roomAccessAttributes } from '../../../../authorization/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; +import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom'; import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; import type { CloseRoomParams } from '../../lib/LivechatTyped'; @@ -178,51 +179,7 @@ API.v1.addRoute( async post() { const { rid, comment, tags, generateTranscriptPdf, transcriptEmail } = this.bodyParams; - const room = await LivechatRooms.findOneById(rid); - if (!room || !isOmnichannelRoom(room)) { - throw new Error('error-invalid-room'); - } - - if (!room.open) { - throw new Error('error-room-already-closed'); - } - - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, this.userId, { projection: { _id: 1 } }); - if (!subscription && !(await hasPermissionAsync(this.userId, 'close-others-livechat-room'))) { - throw new Error('error-not-authorized'); - } - - const options: CloseRoomParams['options'] = { - clientAction: true, - tags, - ...(generateTranscriptPdf && { pdfTranscript: { requestedBy: this.userId } }), - ...(transcriptEmail && { - ...(transcriptEmail.sendToVisitor - ? { - emailTranscript: { - sendToVisitor: true, - requestData: { - email: transcriptEmail.requestData.email, - subject: transcriptEmail.requestData.subject, - requestedAt: new Date(), - requestedBy: this.user, - }, - }, - } - : { - emailTranscript: { - sendToVisitor: false, - }, - }), - }), - }; - - await LivechatTyped.closeRoom({ - room, - user: this.user, - options, - comment, - }); + await closeLivechatRoom(this.user, rid, { comment, tags, generateTranscriptPdf, transcriptEmail }); return API.v1.success(); }, diff --git a/apps/meteor/app/livechat/server/methods/closeRoom.ts b/apps/meteor/app/livechat/server/methods/closeRoom.ts index 1374d86ab9f70..5fdf9e7d504fe 100644 --- a/apps/meteor/app/livechat/server/methods/closeRoom.ts +++ b/apps/meteor/app/livechat/server/methods/closeRoom.ts @@ -60,6 +60,16 @@ Meteor.methods({ }); } + const subscription = await SubscriptionRaw.findOneByRoomIdAndUserId(roomId, userId, { + projection: { + _id: 1, + }, + }); + if (!room.open && subscription) { + await SubscriptionRaw.removeByRoomId(roomId); + return; + } + if (!room.open) { throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:closeRoom' }); } @@ -71,11 +81,6 @@ Meteor.methods({ }); } - const subscription = await SubscriptionRaw.findOneByRoomIdAndUserId(roomId, user._id, { - projection: { - _id: 1, - }, - }); if (!subscription && !(await hasPermissionAsync(userId, 'close-others-livechat-room'))) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:closeRoom', diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 7446d0630b09f..edf5ffcbdc8a5 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -10,6 +10,7 @@ import { useMethod, useTranslation, useRouter, + useUserSubscription, } from '@rocket.chat/ui-contexts'; import React, { useCallback, useState, useEffect } from 'react'; @@ -47,6 +48,7 @@ export const useQuickActions = (): { const visitorRoomId = room.v._id; const rid = room._id; const uid = useUserId(); + const subscription = useUserSubscription(rid); const roomLastMessage = room.lastMessage; const getVisitorInfo = useEndpoint('GET', '/v1/livechat/visitors.info'); @@ -330,7 +332,7 @@ export const useQuickActions = (): { case QuickActionsEnum.TranscriptPDF: return hasLicense && !isRoomOverMacLimit && canSendTranscriptPDF; case QuickActionsEnum.CloseChat: - return !!roomOpen && (canCloseRoom || canCloseOthersRoom); + return (subscription && (canCloseRoom || canCloseOthersRoom)) || (!!roomOpen && canCloseOthersRoom); case QuickActionsEnum.OnHoldChat: return !!roomOpen && canPlaceChatOnHold; default: diff --git a/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts new file mode 100644 index 0000000000000..07ee437832d24 --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/closeLivechatRoom.tests.ts @@ -0,0 +1,154 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../../mocks/data'; + +const subscriptionsStub = { + findOneByRoomIdAndUserId: sinon.stub(), + removeByRoomId: sinon.stub(), + countByRoomId: sinon.stub(), +}; + +const livechatRoomsStub = { + findOneById: sinon.stub(), +}; + +const livechatStub = { + closeRoom: sinon.stub(), +}; + +const hasPermissionStub = sinon.stub(); + +const { closeLivechatRoom } = proxyquire.noCallThru().load('../../../../../../app/lib/server/functions/closeLivechatRoom.ts', { + '../../../livechat/server/lib/LivechatTyped': { + Livechat: livechatStub, + }, + '../../../authorization/server/functions/hasPermission': { + hasPermissionAsync: hasPermissionStub, + }, + '@rocket.chat/models': { + Subscriptions: subscriptionsStub, + LivechatRooms: livechatRoomsStub, + }, +}); + +describe('closeLivechatRoom', () => { + const user = createFakeUser(); + const room = createFakeRoom({ t: 'l', open: true }); + const subscription = createFakeSubscription({ rid: room._id, u: { username: user.username, _id: user._id } }); + + beforeEach(() => { + subscriptionsStub.findOneByRoomIdAndUserId.reset(); + subscriptionsStub.removeByRoomId.reset(); + subscriptionsStub.countByRoomId.reset(); + livechatRoomsStub.findOneById.reset(); + livechatStub.closeRoom.reset(); + hasPermissionStub.reset(); + }); + + it('should not perform any operation when an invalid room id is provided', async () => { + livechatRoomsStub.findOneById.resolves(null); + hasPermissionStub.resolves(true); + + await expect(closeLivechatRoom(user, room._id, {})).to.be.rejectedWith('error-invalid-room'); + expect(livechatStub.closeRoom.notCalled).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; + expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + }); + + it('should not perform any operation when a non-livechat room is provided', async () => { + livechatRoomsStub.findOneById.resolves({ ...room, t: 'c' }); + subscriptionsStub.findOneByRoomIdAndUserId.resolves(subscription); + hasPermissionStub.resolves(true); + + await expect(closeLivechatRoom(user, room._id, {})).to.be.rejectedWith('error-invalid-room'); + expect(livechatStub.closeRoom.notCalled).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; + expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + }); + + it('should not perform any operation when a closed room with no subscriptions is provided and the caller is not subscribed to it', async () => { + livechatRoomsStub.findOneById.resolves({ ...room, open: false }); + subscriptionsStub.countByRoomId.resolves(0); + subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); + hasPermissionStub.resolves(true); + + await expect(closeLivechatRoom(user, room._id, {})).to.be.rejectedWith('error-room-already-closed'); + expect(livechatStub.closeRoom.notCalled).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; + expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + }); + + it('should remove dangling subscription when a closed room with subscriptions is provided and the caller is not subscribed to it', async () => { + livechatRoomsStub.findOneById.resolves({ ...room, open: false }); + subscriptionsStub.countByRoomId.resolves(1); + subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); + hasPermissionStub.resolves(true); + + await closeLivechatRoom(user, room._id, {}); + expect(livechatStub.closeRoom.notCalled).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; + expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; + }); + + it('should remove dangling subscription when a closed room is provided but the user is still subscribed to it', async () => { + livechatRoomsStub.findOneById.resolves({ ...room, open: false }); + subscriptionsStub.findOneByRoomIdAndUserId.resolves(subscription); + subscriptionsStub.countByRoomId.resolves(1); + hasPermissionStub.resolves(true); + + await closeLivechatRoom(user, room._id, {}); + expect(livechatStub.closeRoom.notCalled).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.notCalled).to.be.true; + expect(subscriptionsStub.countByRoomId.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.removeByRoomId.calledOnceWith(room._id)).to.be.true; + }); + + it('should not perform any operation when the caller is not subscribed to an open room and does not have the permission to close others rooms', async () => { + livechatRoomsStub.findOneById.resolves(room); + subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); + subscriptionsStub.countByRoomId.resolves(1); + hasPermissionStub.resolves(false); + + await expect(closeLivechatRoom(user, room._id, {})).to.be.rejectedWith('error-not-authorized'); + expect(livechatStub.closeRoom.notCalled).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.calledOnceWith(room._id, user._id)).to.be.true; + expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + }); + + it('should close the room when the caller is not subscribed to it but has the permission to close others rooms', async () => { + livechatRoomsStub.findOneById.resolves(room); + subscriptionsStub.findOneByRoomIdAndUserId.resolves(null); + subscriptionsStub.countByRoomId.resolves(1); + hasPermissionStub.resolves(true); + + await closeLivechatRoom(user, room._id, {}); + expect(livechatStub.closeRoom.calledOnceWith(sinon.match({ room, user }))).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.calledOnceWith(room._id, user._id)).to.be.true; + expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + }); + + it('should close the room when the caller is subscribed to it and does not have the permission to close others rooms', async () => { + livechatRoomsStub.findOneById.resolves(room); + subscriptionsStub.findOneByRoomIdAndUserId.resolves(subscription); + subscriptionsStub.countByRoomId.resolves(1); + hasPermissionStub.resolves(false); + + await closeLivechatRoom(user, room._id, {}); + expect(livechatStub.closeRoom.calledOnceWith(sinon.match({ room, user }))).to.be.true; + expect(livechatRoomsStub.findOneById.calledOnceWith(room._id)).to.be.true; + expect(subscriptionsStub.findOneByRoomIdAndUserId.calledOnceWith(room._id, user._id)).to.be.true; + expect(subscriptionsStub.removeByRoomId.notCalled).to.be.true; + }); +}); From 3ffe4a29446ea37708cc42dc6fc59d4f2a3f6e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= Date: Tue, 16 Jul 2024 19:02:36 -0300 Subject: [PATCH 050/114] feat: New users page active tab (#32024) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .changeset/rotten-eggs-end.md | 7 ++ .../views/admin/rooms/RoomsTableFilters.tsx | 3 +- .../views/admin/users/AdminUsersPage.tsx | 12 ++- .../admin/users/UsersTable/UsersTable.tsx | 17 ++-- .../users/UsersTable/UsersTableFilters.tsx | 78 +++++++++++++++++++ .../admin/users/hooks/useFilteredUsers.ts | 6 +- packages/i18n/src/locales/en.i18n.json | 3 + .../MultiSelectCustom/MultiSelectCustom.tsx | 26 +++---- .../MultiSelectCustomAnchor.tsx | 4 +- .../MultiSelectCustomList.tsx | 10 +-- 10 files changed, 126 insertions(+), 40 deletions(-) create mode 100644 .changeset/rotten-eggs-end.md create mode 100644 apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx diff --git a/.changeset/rotten-eggs-end.md b/.changeset/rotten-eggs-end.md new file mode 100644 index 0000000000000..7d0ad6ee5047e --- /dev/null +++ b/.changeset/rotten-eggs-end.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": patch +"@rocket.chat/ui-client": patch +--- + +Implemented a new tab to the users page called 'Active', this tab lists all users who have logged in for the first time and are active. diff --git a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx index 1ed21c1234a9a..d52d45415c8a7 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx @@ -9,7 +9,6 @@ const initialRoomTypeFilterStructure = [ { id: 'filter_by_room', text: 'Filter_by_room', - isGroupTitle: true, }, { id: 'd', @@ -71,7 +70,7 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch>; + ); return ( { const isCreateUserDisabled = useShouldPreventAction('activeUsers'); + const getRoles = useEndpoint('GET', '/v1/roles.list'); + const { data } = useQuery(['roles'], async () => getRoles()); + const paginationData = usePagination(); const sortData = useSort('name'); const [tab, setTab] = useState('all'); - const [userFilters, setUserFilters] = useState({ text: '' }); + const [userFilters, setUserFilters] = useState({ text: '', roles: [] }); const searchTerm = useDebouncedValue(userFilters.text, 500); const prevSearchTerm = useRef(''); @@ -70,6 +76,7 @@ const AdminUsersPage = (): ReactElement => { sortData, paginationData, tab, + selectedRoles: useMemo(() => userFilters.roles.map((role) => role.id), [userFilters.roles]), }); const pendingUsersCount = usePendingUsersCount(filteredUsersQueryResult.data?.users); @@ -153,6 +160,7 @@ const AdminUsersPage = (): ReactElement => { sortData={sortData} tab={tab} isSeatsCapExceeded={isSeatsCapExceeded} + roleData={data} /> diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index fa35df715fc57..01d7007561eb7 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -1,13 +1,12 @@ -import type { IAdminUserTabs, Serialized } from '@rocket.chat/core-typings'; +import type { IAdminUserTabs, IRole, Serialized } from '@rocket.chat/core-typings'; import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage'; import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks'; import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import type { ReactElement, Dispatch, SetStateAction } from 'react'; -import React, { useCallback, useMemo } from 'react'; +import React, { useMemo } from 'react'; -import FilterByText from '../../../../components/FilterByText'; import GenericNoResults from '../../../../components/GenericNoResults'; import { GenericTable, @@ -19,10 +18,12 @@ import { import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; import type { useSort } from '../../../../components/GenericTable/hooks/useSort'; import type { UsersFilters, UsersTableSortingOptions } from '../AdminUsersPage'; +import UsersTableFilters from './UsersTableFilters'; import UsersTableRow from './UsersTableRow'; type UsersTableProps = { tab: IAdminUserTabs; + roleData: { roles: IRole[] } | undefined; onReload: () => void; setUserFilters: Dispatch>; filteredUsersQueryResult: UseQueryResult[] }>>; @@ -34,6 +35,7 @@ type UsersTableProps = { const UsersTable = ({ filteredUsersQueryResult, setUserFilters, + roleData, tab, onReload, paginationData, @@ -113,15 +115,10 @@ const UsersTable = ({ [isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab], ); - const handleSearchTextChange = useCallback( - ({ text }) => { - setUserFilters({ text }); - }, - [setUserFilters], - ); return ( <> - + + {isLoading && ( {headers} diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx new file mode 100644 index 0000000000000..28508ac94ac5c --- /dev/null +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx @@ -0,0 +1,78 @@ +import type { IRole } from '@rocket.chat/core-typings'; +import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; +import type { OptionProp } from '@rocket.chat/ui-client'; +import { MultiSelectCustom } from '@rocket.chat/ui-client'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import FilterByText from '../../../../components/FilterByText'; +import type { UsersFilters } from '../AdminUsersPage'; + +type UsersTableFiltersProps = { + setUsersFilters: React.Dispatch>; + roleData: { roles: IRole[] } | undefined; +}; + +const UsersTableFilters = ({ roleData, setUsersFilters }: UsersTableFiltersProps) => { + const { t } = useTranslation(); + + const [selectedRoles, setSelectedRoles] = useState([]); + const [text, setText] = useState(''); + + const handleSearchTextChange = useCallback( + ({ text }) => { + setUsersFilters({ text, roles: selectedRoles }); + setText(text); + }, + [selectedRoles, setUsersFilters], + ); + + const handleRolesChange = useCallback( + (roles: OptionProp[]) => { + setUsersFilters({ text, roles }); + setSelectedRoles(roles); + }, + [setUsersFilters, text], + ); + + const userRolesFilterStructure = useMemo( + () => [ + { + id: 'filter_by_role', + text: 'Filter_by_role', + }, + { + id: 'all', + text: 'All_roles', + checked: false, + }, + ...(roleData + ? roleData.roles.map((role) => ({ + id: role._id, + text: role.description || role.name || role._id, + checked: false, + })) + : []), + ], + [roleData], + ); + + const breakpoints = useBreakpoints(); + const fixFiltersSize = breakpoints.includes('lg') ? { maxWidth: 'x224', minWidth: 'x224' } : null; + + return ( + + + + ); +}; + +export default UsersTableFilters; diff --git a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts index f8ea02a34d826..9a592d5e449fd 100644 --- a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts +++ b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts @@ -15,9 +15,10 @@ type UseFilteredUsersOptions = { tab: IAdminUserTabs; paginationData: ReturnType; sortData: ReturnType>; + selectedRoles: string[]; }; -const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData, tab }: UseFilteredUsersOptions) => { +const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData, tab, selectedRoles }: UseFilteredUsersOptions) => { const { setCurrent, itemsPerPage, current } = paginationData; const { sortBy, sortDirection } = sortData; @@ -45,11 +46,12 @@ const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData return { ...listUsersPayload[tab], searchTerm, + roles: selectedRoles, sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, count: itemsPerPage, offset: searchTerm === prevSearchTerm.current ? current : 0, }; - }, [current, itemsPerPage, prevSearchTerm, searchTerm, setCurrent, sortBy, sortDirection, tab]); + }, [current, itemsPerPage, prevSearchTerm, searchTerm, selectedRoles, setCurrent, sortBy, sortDirection, tab]); const getUsers = useEndpoint('GET', '/v1/users.listByStatus'); const dispatchToastMessage = useToastMessageDispatch(); const usersListQueryResult = useQuery(['users.list', payload, tab], async () => getUsers(payload), { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index f9fcf0163cafb..0b569b4ee5644 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -410,6 +410,7 @@ "AutoLinker_UrlsRegExp": "AutoLinker URL Regular Expression", "All_messages": "All messages", "All_Prices": "All prices", + "All_roles": "All roles", "All_status": "All status", "All_users": "All users", "All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages", @@ -2433,6 +2434,7 @@ "Filter_by_category": "Filter by Category", "Filter_by_Custom_Fields": "Filter by Custom Fields", "Filter_By_Price": "Filter by price", + "Filter_by_role": "Filter by role", "Filter_By_Status": "Filter by status", "Filters": "Filters", "Filters_applied": "Filters applied", @@ -4763,6 +4765,7 @@ "Search_Page_Size": "Page Size", "Search_Private_Groups": "Search Private Groups", "Search_Provider": "Search Provider", + "Search_roles": "Search roles", "Search_rooms": "Search rooms", "Search_Rooms": "Search Rooms", "Search_Users": "Search Users", diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx index affe467cd9659..317f56899d05a 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx @@ -1,7 +1,7 @@ -import { Box } from '@rocket.chat/fuselage'; +import { Box, Button } from '@rocket.chat/fuselage'; import { useOutsideClick, useToggle } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import type { Dispatch, FormEvent, ReactElement, RefObject, SetStateAction } from 'react'; +import type { ComponentProps, FormEvent, ReactElement, RefObject } from 'react'; import { useCallback, useRef } from 'react'; import MultiSelectCustomAnchor from './MultiSelectCustomAnchor'; @@ -21,22 +21,12 @@ const onMouseEventPreventSideEffects = (e: MouseEvent): void => { e.stopImmediatePropagation(); }; -type TitleOptionProp = { +export type OptionProp = { id: string; text: string; - isGroupTitle: boolean; - checked: never; + checked?: boolean; }; -type CheckboxOptionProp = { - id: string; - text: string; - isGroupTitle: never; - checked: boolean; -}; - -export type OptionProp = TitleOptionProp | CheckboxOptionProp; - /** * @param dropdownOptions options available for the multiselect dropdown list * @param defaultTitle dropdown text before selecting any options (or all of them). For example: 'All rooms' @@ -56,9 +46,9 @@ type DropDownProps = { defaultTitle: TranslationKey; selectedOptionsTitle: TranslationKey; selectedOptions: OptionProp[]; - setSelectedOptions: Dispatch>; + setSelectedOptions: (roles: OptionProp[]) => void; searchBarText?: TranslationKey; -}; +} & ComponentProps; export const MultiSelectCustom = ({ dropdownOptions, @@ -67,6 +57,7 @@ export const MultiSelectCustom = ({ selectedOptions, setSelectedOptions, searchBarText, + ...props }: DropDownProps): ReactElement => { const reference = useRef(null); const target = useRef(null); @@ -102,7 +93,7 @@ export const MultiSelectCustom = ({ const count = dropdownOptions.filter((option) => option.checked).length; return ( - + {collapsed && ( diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx index 0a8aee69344bd..3a03673bc701d 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx @@ -14,7 +14,7 @@ type MultiSelectCustomAnchorProps = { } & ComponentProps; const MultiSelectCustomAnchor = forwardRef(function MultiSelectCustomAnchor( - { collapsed, selectedOptionsCount, selectedOptionsTitle, defaultTitle, maxCount, ...props }, + { className, collapsed, selectedOptionsCount, selectedOptionsTitle, defaultTitle, maxCount, ...props }, ref, ) { const t = useTranslation(); @@ -34,7 +34,7 @@ const MultiSelectCustomAnchor = forwardRef {isDirty ? `${t(selectedOptionsTitle)} (${selectedOptionsCount})` : t(defaultTitle)} diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx index d8f8d60d80961..71cb54f81aa57 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx @@ -40,11 +40,7 @@ const MultiSelectCustomList = ({ )} {filteredOptions.map((option) => ( - {option.isGroupTitle ? ( - - {t(option.text as TranslationKey)} - - ) : ( + {option.hasOwnProperty('checked') ? ( + ) : ( + + {t(option.text as TranslationKey)} + )} ))} From 4ba8c04538632afc597d1928b65889ed3950f2c0 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 17 Jul 2024 14:08:03 -0300 Subject: [PATCH 051/114] chore: Renders `CodeMirrorBox`s full screen mode through portal (#32811) --- .../inputs/CodeMirror/CodeMirrorBox.tsx | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx b/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx index 3f61417e4fa68..fd96d715a837b 100644 --- a/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx +++ b/apps/meteor/client/views/admin/settings/inputs/CodeMirror/CodeMirrorBox.tsx @@ -1,51 +1,55 @@ -import { css } from '@rocket.chat/css-in-js'; import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useToggle } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React from 'react'; +import { createPortal } from 'react-dom'; const CodeMirrorBox = ({ label, children }: { label: ReactNode; children: ReactElement }) => { const t = useTranslation(); const [fullScreen, toggleFullScreen] = useToggle(false); - const fullScreenStyle = css` - position: fixed; - z-index: 100; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: flex; - - flex-direction: column; - - width: auto; - height: auto; - - padding: 40px; - - align-items: stretch; - `; - - return ( - - {fullScreen && ( + if (fullScreen) { + return createPortal( + {label} - )} + + {children} + + + + + + + , + document.getElementById('main-content') as HTMLElement, + ); + } + + return ( + {children} From 7d121861d9b92dcf7ac9e72ca14b4c44539a570d Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Date: Wed, 17 Jul 2024 23:24:59 +0530 Subject: [PATCH 052/114] fix: settings disappear on UI navigation back and forth (#32713) --- .changeset/lucky-countries-look.md | 5 +++ .../PrivateSettingsCachedCollection.ts | 2 +- ...tings-persistence-on-ui-navigation.spec.ts | 42 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .changeset/lucky-countries-look.md create mode 100644 apps/meteor/tests/e2e/settings-persistence-on-ui-navigation.spec.ts diff --git a/.changeset/lucky-countries-look.md b/.changeset/lucky-countries-look.md new file mode 100644 index 0000000000000..79deda53edfcb --- /dev/null +++ b/.changeset/lucky-countries-look.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed the disappearance of some settings after navigation under network latency. diff --git a/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts b/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts index b0276e753922e..6f4e1c95a5fa5 100644 --- a/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts +++ b/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts @@ -14,7 +14,7 @@ class PrivateSettingsCachedCollection extends CachedCollection { async setupListener(): Promise { sdk.stream('notify-logged', [this.eventName as 'private-settings-changed'], async (t: string, { _id, ...record }: { _id: string }) => { this.log('record received', t, { _id, ...record }); - this.collection.upsert({ _id }, record); + this.collection.update({ _id }, { $set: record }, { upsert: true }); this.sync(); }); } diff --git a/apps/meteor/tests/e2e/settings-persistence-on-ui-navigation.spec.ts b/apps/meteor/tests/e2e/settings-persistence-on-ui-navigation.spec.ts new file mode 100644 index 0000000000000..f2610d54cd98d --- /dev/null +++ b/apps/meteor/tests/e2e/settings-persistence-on-ui-navigation.spec.ts @@ -0,0 +1,42 @@ +import { Users } from './fixtures/userStates'; +import { setSettingValueById } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('settings-persistence-on-ui-navigation', () => { + test.beforeAll(({ api }) => setSettingValueById(api, 'Hide_System_Messages', [])); + + test.beforeEach(async ({ page }) => { + await page.goto('/admin/settings/Message'); + + // Intercept the API call and delay its response + await page.route('/api/v1/method.call/saveSettings', async (route) => { + const response = await route.fetch(); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Delay the response by 2 seconds + return route.fulfill({ + response, + status: response.status(), + headers: response.headers(), + body: await response.body(), + }); + }); + }); + + test.afterAll(({ api }) => setSettingValueById(api, 'Hide_System_Messages', [])); + + test('expect settings to persist in ui when navigating back and forth', async ({ page }) => { + const settingInput = await page.locator('[data-qa-setting-id="Hide_System_Messages"] input'); + await settingInput.pressSequentially('User joined'); + await settingInput.press('Enter'); + + await page.locator('button:has-text("Save changes")').click(); + await page.locator('button[title="Back"]').click(); + + await page.waitForResponse((response) => response.url().includes('/api/v1/method.call/saveSettings') && response.status() === 200); + + await page.locator('a[href="/admin/settings/Message"] >> text=Open').click(); + + await expect(page.locator('label[for="Hide_System_Messages"][title="Hide_System_Messages"]')).toBeVisible(); + }); +}); From e248432c01f2a59b00d92651d001af181235a053 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 17 Jul 2024 16:43:21 -0300 Subject: [PATCH 053/114] test(Omnichannel): fix close inquiry flaky (#32745) --- .../omnichannel/omnichannel-close-inquiry.spec.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts index 775ea659c1d87..18994c080694f 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts @@ -12,21 +12,22 @@ test.describe('Omnichannel close inquiry', () => { let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; - test.beforeAll(async ({ api, browser }) => { + test.beforeAll(async ({ api }) => { newUser = { name: faker.person.firstName(), email: faker.internet.email(), }; + await api.post('/settings/Livechat_Routing_Method', { value: 'Manual_Selection' }).then((res) => expect(res.status()).toBe(200)); await api.post('/livechat/users/manager', { username: 'user1' }); await api.post('/livechat/users/agent', { username: 'user1' }); - await api.post('/settings/Livechat_Routing_Method', { value: 'Manual_Selection' }).then((res) => expect(res.status()).toBe(200)); - - const { page } = await createAuxContext(browser, Users.user1); - agent = { page, poHomeOmnichannel: new HomeOmnichannel(page) }; }); - test.beforeEach(async ({ page, api }) => { + + test.beforeEach(async ({ page, api, browser }) => { poLiveChat = new OmnichannelLiveChat(page, api); + + const { page: auxPage } = await createAuxContext(browser, Users.user1); + agent = { page: auxPage, poHomeOmnichannel: new HomeOmnichannel(auxPage) }; }); test.afterAll(async ({ api }) => { From bed824fe918a83b57b9a3d76234c73f54f69ac7b Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 17 Jul 2024 18:15:23 -0300 Subject: [PATCH 054/114] fix: Settings list not displaying all groups (#32804) --- .changeset/thin-windows-reply.md | 5 +++++ .../components/Page/PageScrollableContent.tsx | 13 +------------ 2 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 .changeset/thin-windows-reply.md diff --git a/.changeset/thin-windows-reply.md b/.changeset/thin-windows-reply.md new file mode 100644 index 0000000000000..1a32e1ddebfb2 --- /dev/null +++ b/.changeset/thin-windows-reply.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue not displaying all groups in settings list diff --git a/apps/meteor/client/components/Page/PageScrollableContent.tsx b/apps/meteor/client/components/Page/PageScrollableContent.tsx index c3ac6869f277f..f8c3bb5ba54b3 100644 --- a/apps/meteor/client/components/Page/PageScrollableContent.tsx +++ b/apps/meteor/client/components/Page/PageScrollableContent.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import type { Scrollable } from '@rocket.chat/fuselage'; import { Box } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; @@ -26,17 +25,7 @@ const PageScrollableContent = forwardRef - + ); From b8e5887fb9b432282c3e92b4997f8fef0aa68f1b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 17 Jul 2024 17:09:20 -0600 Subject: [PATCH 055/114] feat(apps): add user param to update app call (#32719) Co-authored-by: Douglas Gubert <1810309+d-gubert@users.noreply.github.com> --- .changeset/witty-bats-develop.md | 13 +++++++++++++ apps/meteor/ee/server/apps/communication/rest.ts | 7 ++++++- yarn.lock | 6 +++--- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 .changeset/witty-bats-develop.md diff --git a/.changeset/witty-bats-develop.md b/.changeset/witty-bats-develop.md new file mode 100644 index 0000000000000..42c9409d9ef37 --- /dev/null +++ b/.changeset/witty-bats-develop.md @@ -0,0 +1,13 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/apps": patch +"@rocket.chat/core-services": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/fuselage-ui-kit": patch +"@rocket.chat/rest-typings": patch +"@rocket.chat/ddp-streamer": patch +"@rocket.chat/presence": patch +"rocketchat-services": patch +--- + +Added the `user` param to apps-engine update method call, allowing apps' new `onUpdate` hook to know who triggered the update. diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 02f8aeb7b3446..3eed45f4ddad6 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -747,7 +747,12 @@ export class AppsRestApi { return API.v1.internalError('private_app_install_disabled'); } - const aff = await manager.update(buff, permissionsGranted); + const user = orchestrator + ?.getConverters() + ?.get('users') + ?.convertToApp(await Meteor.userAsync()); + + const aff = await manager.update(buff, permissionsGranted, { user, loadApp: true }); const info: IAppInfo & { status?: AppStatus } = aff.getAppInfo(); if (aff.hasStorageError()) { diff --git a/yarn.lock b/yarn.lock index add9a5e19468d..e7555d99208ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8510,8 +8510,8 @@ __metadata: linkType: soft "@rocket.chat/apps-engine@npm:alpha": - version: 1.44.0-alpha.814 - resolution: "@rocket.chat/apps-engine@npm:1.44.0-alpha.814" + version: 1.44.0-alpha.818 + resolution: "@rocket.chat/apps-engine@npm:1.44.0-alpha.818" dependencies: "@msgpack/msgpack": 3.0.0-beta2 adm-zip: ^0.5.9 @@ -8527,7 +8527,7 @@ __metadata: uuid: ~8.3.2 peerDependencies: "@rocket.chat/ui-kit": "*" - checksum: 03f777ecd035af20c88558a941db77bb104f4402f78169a6d23e1629613c11ac23c04f6bdfb451a4273558789de58d4567661a7bd5de91b78464f98698e5f6a6 + checksum: acef47bc7f13e0682d018531638b5168c3acd59beae37b013e881ea6fadfe12670fe10545f4a89487f7bedbe9166028833cba7ed3fc401d4283327e47e00e61c languageName: node linkType: hard From c3e9c616bf8dd2c990cbe9fb675d3e9e2a8c8352 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Jul 2024 14:28:09 -0300 Subject: [PATCH 056/114] test: close pages after page spec (#32830) --- .../tests/e2e/omnichannel/omnichannel-livechat.spec.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts index 00823cd98a195..24110a15eb257 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts @@ -42,7 +42,7 @@ test.describe.serial('OC - Livechat', () => { test.afterAll(async ({ api }) => { await api.delete('/livechat/users/agent/user1'); - await poLiveChat.page?.close(); + await poLiveChat.page.close(); }); test('OC - Livechat - Send message to online agent', async () => { @@ -123,7 +123,8 @@ test.describe.serial('OC - Livechat - Resub after close room', () => { test.afterAll(async ({ api }) => { await api.post('/settings/Livechat_clear_local_storage_when_chat_ended', { value: false }); await api.delete('/livechat/users/agent/user1'); - await poLiveChat.page?.close(); + await poLiveChat.page.close(); + await poHomeOmnichannel.page.close(); }); test('OC - Livechat - Resub after close room', async () => { @@ -169,7 +170,8 @@ test.describe('OC - Livechat - Resume chat after closing', () => { test.afterAll(async ({ api }) => { await api.delete('/livechat/users/agent/user1'); - await poLiveChat.page?.close(); + await poLiveChat.page.close(); + await poHomeOmnichannel.page.close(); }); test('OC - Livechat - Resume chat after closing', async () => { @@ -213,7 +215,7 @@ test.describe('OC - Livechat - Close chat using widget', () => { }); test.afterAll(async () => { - await poHomeOmnichannel.page?.close(); + await poHomeOmnichannel.page.close(); await agent.delete(); }); From 3cfd6dd9ce0a39eb4604d687d1c68e5d2f9f1d26 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Jul 2024 15:47:58 -0300 Subject: [PATCH 057/114] test: close pages after page spec (#32832) --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 2 +- .../omnichannel-auto-onhold-chat-closing.spec.ts | 4 ++-- ...ichannel-auto-transfer-unanswered-chat.spec.ts | 6 +++--- ...channel-changing-room-priority-and-sla.spec.ts | 15 ++++++--------- .../omnichannel/omnichannel-close-inquiry.spec.ts | 5 ++++- ...omnichannel-livechat-avatar-visibility.spec.ts | 2 +- .../omnichannel-livechat-department.spec.ts | 4 ++-- .../omnichannel-livechat-fileupload.spec.ts | 2 +- .../omnichannel/omnichannel-livechat-logo.spec.ts | 4 ++++ ...omnichannel-livechat-tab-communication.spec.ts | 9 ++++++--- .../omnichannel-livechat-watermark.spec.ts | 4 ++++ .../omnichannel-manual-selection.spec.ts | 4 +++- .../e2e/omnichannel/omnichannel-tags.spec.ts | 1 + .../e2e/omnichannel/omnichannel-takeChat.spec.ts | 2 -- ...omnichannel-transfer-to-another-agents.spec.ts | 6 +++--- 15 files changed, 41 insertions(+), 29 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 0cc344ff51527..7bdbbf8b19f41 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -272,7 +272,7 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async persistKeys( + private async persistKeys( { public_key, private_key }: KeyPair, password: string, { force }: { force: boolean } = { force: false }, diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts index 7f321342b5026..28a38afa12383 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts @@ -27,13 +27,13 @@ test.describe('omnichannel-auto-onhold-chat-closing', () => { agent = { page, poHomeChannel: new HomeChannel(page) }; }); test.afterAll(async ({ api }) => { + await agent.page.close(); + await Promise.all([ api.delete('/livechat/users/agent/user1').then((res) => expect(res.status()).toBe(200)), api.post('/settings/Livechat_auto_close_on_hold_chats_timeout', { value: 3600 }).then((res) => expect(res.status()).toBe(200)), api.post('/settings/Livechat_allow_manual_on_hold', { value: false }).then((res) => expect(res.status()).toBe(200)), ]); - - await agent.page.close(); }); test.beforeEach(async ({ page, api }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts index a1620f3d70c7e..c1516e0a50324 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts @@ -32,14 +32,14 @@ test.describe('omnichannel-auto-transfer-unanswered-chat', () => { }); test.afterAll(async ({ api }) => { + await agent1.page.close(); + await agent2.page.close(); + await Promise.all([ api.delete('/livechat/users/agent/user1').then((res) => expect(res.status()).toBe(200)), api.delete('/livechat/users/agent/user2').then((res) => expect(res.status()).toBe(200)), api.post('/settings/Livechat_auto_transfer_chat_timeout', { value: 0 }).then((res) => expect(res.status()).toBe(200)), ]); - - await agent1.page.close(); - await agent2.page.close(); }); test.beforeEach(async ({ page, api }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts index 694dbff94ebed..765dc33a84608 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts @@ -45,16 +45,13 @@ test.describe.serial('omnichannel-changing-room-priority-and-sla', () => { }); test.afterAll(async ({ api }) => { - let statusCode = (await api.delete(`/livechat/users/agent/${ADMIN_CREDENTIALS.username}`)).status(); - await expect(statusCode).toBe(200); - - statusCode = (await api.delete(`/livechat/users/manager/${ADMIN_CREDENTIALS.username}`)).status(); - await expect(statusCode).toBe(200); - - statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' })).status(); - await expect(statusCode).toBe(200); - await agent.page.close(); + + await Promise.all([ + api.delete(`/livechat/users/agent/${ADMIN_CREDENTIALS.username}`), + api.delete(`/livechat/users/manager/${ADMIN_CREDENTIALS.username}`), + api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }), + ]); }); test('expect to initiate a new livechat conversation', async ({ page, api }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts index 18994c080694f..51ec1b4376bcc 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts @@ -30,13 +30,16 @@ test.describe('Omnichannel close inquiry', () => { agent = { page: auxPage, poHomeOmnichannel: new HomeOmnichannel(auxPage) }; }); + test.afterEach(async () => { + await agent.page.close(); + }); + test.afterAll(async ({ api }) => { await Promise.all([ await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }).then((res) => expect(res.status()).toBe(200)), await api.delete('/livechat/users/agent/user1'), await api.delete('/livechat/users/manager/user1'), ]); - await agent.page.close(); }); test('Receiving a message from visitor', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts index d2b1db457ba1b..e26c5548d403f 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts @@ -49,7 +49,7 @@ test.describe('OC - Livechat - Avatar visibility', async () => { }); test.afterEach(async ({ page }) => { - await poAuxContext.page?.close(); + await poAuxContext.page.close(); await page.close(); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts index d71467a9336c2..21ffb82db1c72 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts @@ -49,8 +49,8 @@ test.describe('OC - Livechat - Department Flow', () => { }); test.afterEach(async ({ page }) => { - await poHomeOmnichannelAgent1?.page?.close(); - await poHomeOmnichannelAgent2?.page?.close(); + await poHomeOmnichannelAgent1.page.close(); + await poHomeOmnichannelAgent2.page.close(); await page.close(); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts index b1244e7813b73..2eed3c1421267 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts @@ -52,7 +52,7 @@ test.describe('OC - Livechat - OC - File Upload', () => { await api.post('/settings/FileUpload_Enabled', { value: true }); await api.post('/settings/Livechat_fileupload_enabled', { value: true }); - await poHomeOmnichannel.page?.close(); + await poHomeOmnichannel.page.close(); await agent.delete(); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts index c7e70a8279820..51f59b5af2879 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts @@ -18,6 +18,10 @@ test.describe('OC - Livechat - Widget logo', async () => { poLiveChat = new OmnichannelLiveChat(livechatPage, api); }); + test.afterEach(async () => { + await poLiveChat.page.close(); + }); + test.beforeEach(async ({ page }) => { poOmnichannelSettings = new OmnichannelSettings(page); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts index 60e2e193262b5..386993a9b6a5c 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts @@ -32,13 +32,16 @@ test.describe('OC - Livechat - Cross Tab Communication', () => { await pageLivechat2.page.goto('/livechat'); }); - test.afterAll(async () => { - await poHomeOmnichannel.page?.close(); - await agent.delete(); + test.afterEach(async () => { await pageLivechat1.page.close(); await pageLivechat2.page.close(); }); + test.afterAll(async () => { + await poHomeOmnichannel.page.close(); + await agent.delete(); + }); + test('OC - Livechat - Send messages, close chat and start again 2 tabs', async () => { const firstUser = { name: `${faker.person.firstName()} ${faker.string.uuid()}}`, diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts index d0dd0c76f2977..fc8be5b69c814 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts @@ -35,6 +35,10 @@ test.describe('OC - Livechat - Hide watermark', async () => { poLiveChat = new OmnichannelLiveChat(livechatPage, api); }); + test.afterEach(async () => { + await poLiveChat.page.close(); + }); + test.beforeEach(async ({ page }) => { poOmnichannelSettings = new OmnichannelSettings(page); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts index 8a4e74c1661a2..ff7245e0b5324 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts @@ -39,10 +39,12 @@ test.describe('OC - Manual Selection', () => { agentB = await createAuxContext(browser, Users.user1).then(({ page }) => ({ page, poHomeOmnichannel: new HomeOmnichannel(page) })); }); + test.afterEach(async () => { + await agentB.page.close(); + }); // Delete all data test.afterAll(async ({ api }) => { await Promise.all([ - agentB.page.close(), ...agents.map((agent) => agent.delete()), api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }), ]); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts index f14cf6c374458..7b74b3d7cd87a 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts @@ -31,6 +31,7 @@ test.describe('OC - Manage Tags', () => { test.afterAll(async () => { await department.delete(); + await department2.delete(); await agent.delete(); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts index f7cacd5805d76..399c9394740e0 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts @@ -68,8 +68,6 @@ test.describe('omnichannel-takeChat', () => { await expect(agent.poHomeChannel.content.btnTakeChat).not.toBeVisible(); await expect(agent.poHomeChannel.content.inputMessage).toBeVisible(); - - await poLiveChat.closeChat(); }); test('When agent is offline should not take the chat', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts index 272cde5c2c493..5be6c4181460b 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts @@ -45,19 +45,19 @@ test.describe('OC - Chat transfers [Agent role]', () => { }); // Make "user-1" online & "user-2" offline so that chat can be automatically routed to "user-1" - test.beforeEach(async () => { + test.beforeAll(async () => { const [agentA, agentB] = sessions; await agentA.poHomeOmnichannel.sidenav.switchStatus('online'); await agentB.poHomeOmnichannel.sidenav.switchStatus('offline'); }); // Close sessions - test.afterEach(async () => { + test.afterAll(async () => { await Promise.all(sessions.map(({ page }) => page.close())); }); // Start a new chat for each test - test.beforeEach(async ({ api }) => { + test.beforeAll(async ({ api }) => { conversations = [await createConversation(api)]; }); From 2febca263e06c5dc90e33cd82c4a3704cff4a013 Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Date: Fri, 19 Jul 2024 01:27:47 +0530 Subject: [PATCH 058/114] fix: bad word filtering not working (#32810) --- .changeset/funny-wolves-tie.md | 5 ++ .../server/services/messages/service.ts | 2 +- apps/meteor/tests/e2e/permissions.spec.ts | 2 +- apps/meteor/tests/end-to-end/api/chat.ts | 67 +++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 .changeset/funny-wolves-tie.md diff --git a/.changeset/funny-wolves-tie.md b/.changeset/funny-wolves-tie.md new file mode 100644 index 0000000000000..e2364ccb05e50 --- /dev/null +++ b/.changeset/funny-wolves-tie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed issue where bad word filtering was not working in the UI for messages diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index e18a9c276c503..03906bfd0208e 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -142,8 +142,8 @@ export class MessageService extends ServiceClassInternal implements IMessageServ message = await mentionServer.execute(message); message = await this.cannedResponse.replacePlaceholders({ message, room, user }); - message = await this.markdownParser.parseMarkdown({ message, config: this.getMarkdownConfig() }); message = await this.badWords.filterBadWords({ message }); + message = await this.markdownParser.parseMarkdown({ message, config: this.getMarkdownConfig() }); message = await this.spotify.convertSpotifyLinks({ message }); message = await this.jumpToMessage.createAttachmentForMessageURLs({ message, diff --git a/apps/meteor/tests/e2e/permissions.spec.ts b/apps/meteor/tests/e2e/permissions.spec.ts index 54aaeb3c0ee59..75bf44a159e20 100644 --- a/apps/meteor/tests/e2e/permissions.spec.ts +++ b/apps/meteor/tests/e2e/permissions.spec.ts @@ -186,7 +186,7 @@ test.describe.serial('permissions', () => { }); }); - test.describe.skip('Filter words', () => { + test.describe.serial('Filter words', () => { test.beforeAll(async ({ api }) => { const statusCode1 = (await api.post('/settings/Message_AllowBadWordsFilter', { value: true })).status(); const statusCode2 = (await api.post('/settings/Message_BadWordsFilterList', { value: 'badword' })).status(); diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index e41a4232d9082..82a1a68955ed4 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -1,5 +1,6 @@ import type { Credentials } from '@rocket.chat/api-client'; import type { IMessage, IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { after, before, beforeEach, describe, it } from 'mocha'; import type { Response } from 'supertest'; @@ -768,6 +769,40 @@ describe('[Chat]', () => { .end(done); }); + describe('Bad words filter', () => { + before(() => + Promise.all([updateSetting('Message_AllowBadWordsFilter', true), updateSetting('Message_BadWordsFilterList', 'badword,badword2')]), + ); + + after(() => Promise.all([updateSetting('Message_AllowBadWordsFilter', false), updateSetting('Message_BadWordsFilterList', '')])); + + it('should censor bad words on send', async () => { + const badMessage = { + _id: Random.id(), + rid: testChannel._id, + msg: 'This message has badword badword2', + }; + + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ message: badMessage }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + const { message } = res.body; + expect(message).to.have.property('msg', 'This message has ******* ********'); + expect(message).to.have.property('md').to.be.an('array').that.has.lengthOf(1); + const para = message.md[0]; + expect(para).to.have.property('value').to.be.an('array').that.has.lengthOf(1); + const text = para.value[0]; + expect(text).to.have.property('value', 'This message has ******* ********'); + }); + }); + }); + describe('oembed', () => { let ytEmbedMsgId: IMessage['_id']; let imgUrlMsgId: IMessage['_id']; @@ -1460,6 +1495,38 @@ describe('[Chat]', () => { expect(res.body.message).to.have.property('attachments').that.is.an('array').that.has.lengthOf(0); }); }); + + describe('Bad words filter', () => { + before(() => + Promise.all([updateSetting('Message_AllowBadWordsFilter', true), updateSetting('Message_BadWordsFilterList', 'badword,badword2')]), + ); + + after(() => Promise.all([updateSetting('Message_AllowBadWordsFilter', false), updateSetting('Message_BadWordsFilterList', '')])); + + it('should censor bad words on update', async () => { + await request + .post(api('chat.update')) + .set(credentials) + .send({ + roomId: testChannel._id, + msgId: message._id, + text: 'This message has badword badword2', + }) + .expect(200) + .expect('Content-Type', 'application/json') + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + const { message } = res.body; + expect(message).to.have.property('msg', 'This message has ******* ********'); + expect(message).to.have.property('md').to.be.an('array').that.has.lengthOf(1); + const para = message.md[0]; + expect(para).to.have.property('value').to.be.an('array').that.has.lengthOf(1); + const text = para.value[0]; + expect(text).to.have.property('value', 'This message has ******* ********'); + }); + }); + }); }); describe('[/chat.delete]', () => { From e827c58abbb674740b77b877546178c89661b0aa Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Jul 2024 17:36:40 -0300 Subject: [PATCH 059/114] fix: imported fixes (#32815) --- .changeset/violet-brooms-press.md | 5 +++++ apps/meteor/app/discussion/client/index.ts | 1 - .../client/lib/messageTypes/discussionMessage.js | 16 ---------------- apps/meteor/app/ui-utils/lib/MessageTypes.ts | 2 -- apps/meteor/app/ui-utils/server/Message.ts | 3 --- .../message/variants/SystemMessage.tsx | 11 +++-------- 6 files changed, 8 insertions(+), 30 deletions(-) create mode 100644 .changeset/violet-brooms-press.md delete mode 100644 apps/meteor/app/discussion/client/lib/messageTypes/discussionMessage.js diff --git a/.changeset/violet-brooms-press.md b/.changeset/violet-brooms-press.md new file mode 100644 index 0000000000000..632026d6fe2e1 --- /dev/null +++ b/.changeset/violet-brooms-press.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) diff --git a/apps/meteor/app/discussion/client/index.ts b/apps/meteor/app/discussion/client/index.ts index 62e11191b4936..7c0a6f72e6cc0 100644 --- a/apps/meteor/app/discussion/client/index.ts +++ b/apps/meteor/app/discussion/client/index.ts @@ -1,3 +1,2 @@ // Other UI extensions -import './lib/messageTypes/discussionMessage'; import './createDiscussionMessageAction'; diff --git a/apps/meteor/app/discussion/client/lib/messageTypes/discussionMessage.js b/apps/meteor/app/discussion/client/lib/messageTypes/discussionMessage.js deleted file mode 100644 index a7f0ef0a1d977..0000000000000 --- a/apps/meteor/app/discussion/client/lib/messageTypes/discussionMessage.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { MessageTypes } from '../../../../ui-utils/client'; - -Meteor.startup(() => { - MessageTypes.registerType({ - id: 'discussion-created', - system: false, - message: 'discussion-created', - data(message) { - return { - message: ` ${message.msg}`, - }; - }, - }); -}); diff --git a/apps/meteor/app/ui-utils/lib/MessageTypes.ts b/apps/meteor/app/ui-utils/lib/MessageTypes.ts index a4f77d10cbf7b..c108fe55f1684 100644 --- a/apps/meteor/app/ui-utils/lib/MessageTypes.ts +++ b/apps/meteor/app/ui-utils/lib/MessageTypes.ts @@ -5,8 +5,6 @@ export type MessageType = { id: MessageTypesValues; system?: boolean; /* deprecated */ - render?: (message: IMessage) => string; - /* deprecated */ template?: (message: IMessage) => unknown; message: TranslationKey; data?: (message: IMessage) => Record; diff --git a/apps/meteor/app/ui-utils/server/Message.ts b/apps/meteor/app/ui-utils/server/Message.ts index 18cf842b19936..06ae59238b429 100644 --- a/apps/meteor/app/ui-utils/server/Message.ts +++ b/apps/meteor/app/ui-utils/server/Message.ts @@ -11,9 +11,6 @@ export const Message = { parse(msg: IMessage, language: string) { const messageType = MessageTypes.getType(msg); if (messageType) { - if (messageType.render) { - return messageType.render(msg); - } if (messageType.template) { // Render message return; diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index e2e1d9bf04bdd..eeba342c3f31e 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -94,14 +94,9 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps )} {messageType && ( - + + {t(messageType.message, messageType.data ? messageType.data(message) : {})} + )} {formatTime(message.ts)} From a87289db32c197a58fc90dfe9f8235a514d356b7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 18 Jul 2024 20:57:59 -0300 Subject: [PATCH 060/114] chore(omnichannel): cache room and users between bridges (#32839) --- .../apps/server/converters/cachedFunction.ts | 17 +++++++++++++++++ .../app/apps/server/converters/messages.js | 18 +++++++++++++++--- .../app/apps/server/converters/threads.ts | 19 +------------------ 3 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 apps/meteor/app/apps/server/converters/cachedFunction.ts diff --git a/apps/meteor/app/apps/server/converters/cachedFunction.ts b/apps/meteor/app/apps/server/converters/cachedFunction.ts new file mode 100644 index 0000000000000..3310574f01608 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/cachedFunction.ts @@ -0,0 +1,17 @@ +export const cachedFunction = any>(fn: F) => { + const cache = new Map(); + + return ((...args) => { + const cacheKey = JSON.stringify(args); + + if (cache.has(cacheKey)) { + return cache.get(cacheKey) as ReturnType; + } + + const result = fn(...args); + + cache.set(cacheKey, result); + + return result; + }) as F; +}; diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 187a6519339a4..9804e0bb3b2c2 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -1,9 +1,12 @@ import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { cachedFunction } from './cachedFunction'; import { transformMappedData } from './transformMappedData'; export class AppMessagesConverter { + mem = new WeakMap(); + constructor(orch) { this.orch = orch; } @@ -19,6 +22,15 @@ export class AppMessagesConverter { return undefined; } + const cache = + this.mem.get(msgObj) ?? + new Map([ + ['room', cachedFunction(this.orch.getConverters().get('rooms').convertById.bind(this.orch.getConverters().get('rooms')))], + ['user', cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users')))], + ]); + + this.mem.set(msgObj, cache); + const map = { id: '_id', threadId: 'tmid', @@ -37,7 +49,7 @@ export class AppMessagesConverter { token: 'token', blocks: 'blocks', room: async (message) => { - const result = await this.orch.getConverters().get('rooms').convertById(message.rid); + const result = await cache.get('room')(message.rid); delete message.rid; return result; }, @@ -49,7 +61,7 @@ export class AppMessagesConverter { return undefined; } - return this.orch.getConverters().get('users').convertById(editedBy._id); + return cache.get('user')(editedBy._id); }, attachments: async (message) => { const result = await this._convertAttachmentsToApp(message.attachments); @@ -61,7 +73,7 @@ export class AppMessagesConverter { return undefined; } - let user = await this.orch.getConverters().get('users').convertById(message.u._id); + let user = await cache.get('user')(message.u._id); // When the sender of the message is a Guest (livechat) and not a user if (!user) { diff --git a/apps/meteor/app/apps/server/converters/threads.ts b/apps/meteor/app/apps/server/converters/threads.ts index 840f4f1613ebb..e31ee094b4d73 100644 --- a/apps/meteor/app/apps/server/converters/threads.ts +++ b/apps/meteor/app/apps/server/converters/threads.ts @@ -5,6 +5,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { isEditedMessage, type IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; +import { cachedFunction } from './cachedFunction'; import { transformMappedData } from './transformMappedData'; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -18,24 +19,6 @@ interface Orchestrator { }; } -const cachedFunction = any>(fn: F) => { - const cache = new Map(); - - return ((...args) => { - const cacheKey = JSON.stringify(args); - - if (cache.has(cacheKey)) { - return cache.get(cacheKey) as ReturnType; - } - - const result = fn(...args); - - cache.set(cacheKey, result); - - return result; - }) as F; -}; - export class AppThreadsConverter implements IAppThreadsConverter { constructor( private readonly orch: { From 6237ebfaee44b167edea97dc2892ac4008590520 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 19 Jul 2024 03:06:16 -0300 Subject: [PATCH 061/114] test(e2e): Avoid duplicated visitor names (#32840) --- .../omnichannel/omnichannel-agents.spec.ts | 2 +- ...nichannel-auto-onhold-chat-closing.spec.ts | 7 +- ...nnel-auto-transfer-unanswered-chat.spec.ts | 7 +- ...nichannel-canned-responses-sidebar.spec.ts | 13 +-- ...nel-changing-room-priority-and-sla.spec.ts | 13 +-- .../omnichannel-chat-history.spec.ts | 15 ++- .../omnichannel-close-chat.spec.ts | 13 +-- .../omnichannel-close-inquiry.spec.ts | 15 ++- .../omnichannel-contact-info.spec.ts | 13 +-- .../omnichannel-livechat-api.spec.ts | 102 +++++------------- ...channel-livechat-avatar-visibility.spec.ts | 9 +- .../omnichannel-livechat-background.spec.ts | 10 +- .../omnichannel-livechat-department.spec.ts | 13 +-- .../omnichannel-livechat-fileupload.spec.ts | 8 +- ...nnel-livechat-message-bubble-color.spec.ts | 21 ++-- ...channel-livechat-tab-communication.spec.ts | 10 +- .../omnichannel-livechat-watermark.spec.ts | 8 +- .../omnichannel/omnichannel-livechat.spec.ts | 41 +++---- .../omnichannel-priorities-sidebar.spec.ts | 24 ++--- .../omnichannel-send-pdf-transcript.spec.ts | 15 ++- .../omnichannel-send-transcript.spec.ts | 13 +-- .../omnichannel/omnichannel-takeChat.spec.ts | 7 +- ...hannel-triggers-after-registration.spec.ts | 16 ++- ...nichannel-triggers-open-by-visitor.spec.ts | 11 +- .../omnichannel-triggers-time-on-site.spec.ts | 11 +- .../omnichannel/omnichannel-triggers.spec.ts | 14 ++- apps/meteor/tests/mocks/data.ts | 19 ++++ 27 files changed, 167 insertions(+), 283 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts index 70b5e51f681c9..ad4657b1841c9 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts @@ -25,7 +25,7 @@ test.describe.serial('OC - Manage Agents', () => { // Ensure that there is no leftover data even if test fails test.afterEach(async ({ api }) => { - await await api.delete('/livechat/users/agent/user1'); + await api.delete('/livechat/users/agent/user1'); await api.post('/settings/Omnichannel_enable_department_removal', { value: true }).then((res) => expect(res.status()).toBe(200)); await department.delete(); await api.post('/settings/Omnichannel_enable_department_removal', { value: false }).then((res) => expect(res.status()).toBe(200)); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts index 28a38afa12383..255ac0d3ed091 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -41,10 +41,7 @@ test.describe('omnichannel-auto-onhold-chat-closing', () => { await agent.poHomeChannel.sidenav.switchStatus('online'); // start a new chat for each test - newVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); poLiveChat = new OmnichannelLiveChat(page, api); await page.goto('/livechat'); await poLiveChat.openLiveChat(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts index c1516e0a50324..285fdaef828a5 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -48,10 +48,7 @@ test.describe('omnichannel-auto-transfer-unanswered-chat', () => { await agent2.poHomeChannel.sidenav.switchOmnichannelStatus('offline'); // start a new chat for each test - newVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); poLiveChat = new OmnichannelLiveChat(page, api); await page.goto('/livechat'); await poLiveChat.openLiveChat(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts index b0c7c22491b6e..8d3c3a4283f25 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -11,15 +11,12 @@ test.describe('Omnichannel Canned Responses Sidebar', () => { test.skip(!IS_EE, 'Enterprise Only'); let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeChannel: HomeChannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); // Set user user 1 as manager and agent await api.post('/livechat/users/agent', { username: 'user1' }); @@ -42,13 +39,13 @@ test.describe('Omnichannel Canned Responses Sidebar', () => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeChannel.sidenav.openChat(newUser.name); + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to be able to open canned responses sidebar and creation', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts index 765dc33a84608..ee32eb4efe0de 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { ADMIN_CREDENTIALS, IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -30,13 +30,13 @@ test.describe.serial('omnichannel-changing-room-priority-and-sla', () => { test.beforeAll(async ({ api, browser }) => { let statusCode = (await api.post('/livechat/users/agent', { username: ADMIN_CREDENTIALS.username })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); statusCode = (await api.post('/livechat/users/manager', { username: ADMIN_CREDENTIALS.username })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Manual_Selection' })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); const { page } = await createAuxContext(browser, Users.admin); agent = { page, poHomeChannel: new HomeChannel(page) }; @@ -55,10 +55,7 @@ test.describe.serial('omnichannel-changing-room-priority-and-sla', () => { }); test('expect to initiate a new livechat conversation', async ({ page, api }) => { - newVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); poLiveChat = new OmnichannelLiveChat(page, api); await page.goto('/livechat'); await poLiveChat.openLiveChat(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts index 044d3df516f0a..2be6347a3e0e1 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -8,15 +8,12 @@ import { test, expect } from '../utils/test'; test.describe('Omnichannel chat histr', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); // Set user user 1 as manager and agent await api.post('/livechat/users/agent', { username: 'user1' }); @@ -38,13 +35,13 @@ test.describe('Omnichannel chat histr', () => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeOmnichannel.sidenav.openChat(newUser.name); + await agent.poHomeOmnichannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to be able to close an omnichannel to conversation', async () => { @@ -62,7 +59,7 @@ test.describe('Omnichannel chat histr', () => { }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeOmnichannel.sidenav.openChat(newUser.name); + await agent.poHomeOmnichannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to be able to see conversation history', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts index 75fb398ee1dec..b17842afd969d 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -8,15 +8,12 @@ import { test, expect } from '../utils/test'; test.describe('Omnichannel close chat', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); // Set user user 1 as manager and agent await api.post('/livechat/users/agent', { username: 'user1' }); @@ -39,13 +36,13 @@ test.describe('Omnichannel close chat', () => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeOmnichannel.sidenav.openChat(newUser.name); + await agent.poHomeOmnichannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to be able to close an omnichannel to conversation', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts index 51ec1b4376bcc..b37a34ba861e7 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -8,15 +8,12 @@ import { test, expect } from '../utils/test'; test.describe('Omnichannel close inquiry', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); await api.post('/settings/Livechat_Routing_Method', { value: 'Manual_Selection' }).then((res) => expect(res.status()).toBe(200)); await api.post('/livechat/users/manager', { username: 'user1' }); @@ -46,13 +43,13 @@ test.describe('Omnichannel close inquiry', () => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeOmnichannel.sidenav.getQueuedChat(newUser.name).click(); + await agent.poHomeOmnichannel.sidenav.getQueuedChat(newVisitor.name).click(); await expect(agent.poHomeOmnichannel.content.btnTakeChat).toBeVisible(); }); @@ -66,7 +63,7 @@ test.describe('Omnichannel close inquiry', () => { await test.step('Expect to inquiry be closed when navigate back', async () => { await agent.poHomeOmnichannel.sidenav.openAdministrationByLabel('Omnichannel'); await agent.poHomeOmnichannel.omnisidenav.linkCurrentChats.click(); - await agent.poHomeOmnichannel.currentChats.findRowByName(newUser.name).click(); + await agent.poHomeOmnichannel.currentChats.findRowByName(newVisitor.name).click(); await expect(agent.poHomeOmnichannel.content.btnTakeChat).not.toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts index 266df1849292c..76b8ac386ef78 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; @@ -8,15 +8,12 @@ import { test } from '../utils/test'; test.describe('Omnichannel contact info', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeChannel: HomeChannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); // Set user user 1 as manager and agent await api.post('/livechat/users/agent', { username: 'user1' }); @@ -39,13 +36,13 @@ test.describe('Omnichannel contact info', () => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeChannel.sidenav.openChat(newUser.name); + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to be see contact information and edit', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts index 7250fce9e24e9..e529746def9ea 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts @@ -1,6 +1,7 @@ -import { faker } from '@faker-js/faker'; +import { faker } from '@faker-js/faker/locale/af_ZA'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -57,6 +58,12 @@ declare const window: Window & { }; }; +const createFakeVisitorRegistration = (extra?: { department?: string }) => ({ + ...createFakeVisitor(), + token: faker.string.uuid(), + ...extra, +}); + test.describe('OC - Livechat API', () => { // TODO: Check if there is a way to add livechat to the global window object @@ -142,11 +149,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - setTheme', async () => { - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; + const registerGuestVisitor = createFakeVisitorRegistration(); await test.step('Expect setTheme set color', async () => { await poLiveChat.page.evaluate(() => { @@ -229,7 +232,7 @@ test.describe('OC - Livechat API', () => { await addAgentToDepartment(api, { department: departmentA, agentId: agent.data._id }); await addAgentToDepartment(api, { department: departmentB, agentId: agent2.data._id }); - await expect((await api.post('/settings/Livechat_offline_email', { value: 'test@testing.com' })).status()).toBe(200); + expect((await api.post('/settings/Livechat_offline_email', { value: 'test@testing.com' })).status()).toBe(200); }); test.beforeEach(async ({ browser }, testInfo) => { @@ -324,12 +327,9 @@ test.describe('OC - Livechat API', () => { test('setDepartment - Called during ongoing conversation', async () => { const [departmentA, departmentB] = departments.map(({ data }) => data); - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), + const registerGuestVisitor = createFakeVisitorRegistration({ department: departmentA._id, - }; + }); // Start Chat await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); @@ -370,10 +370,7 @@ test.describe('OC - Livechat API', () => { test('setDepartment - Called before conversation', async () => { const departmentB = departments[1].data; - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + const registerGuestVisitor = createFakeVisitor(); const depId = departmentB._id; @@ -421,12 +418,9 @@ test.describe('OC - Livechat API', () => { test('transferChat - Called during ongoing conversation', async () => { const [departmentA, departmentB] = departments.map(({ data }) => data); - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), + const registerGuestVisitor = createFakeVisitorRegistration({ department: departmentA._id, - }; + }); // Start Chat await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); @@ -462,11 +456,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - registerGuest', async ({ browser }) => { - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; + const registerGuestVisitor = createFakeVisitorRegistration(); await test.step('Expect registerGuest to create a valid guest', async () => { await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); @@ -510,17 +500,9 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - registerGuest different guests', async () => { - const registerGuestVisitor1 = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; - - const registerGuestVisitor2 = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; + const registerGuestVisitor1 = createFakeVisitorRegistration(); + + const registerGuestVisitor2 = createFakeVisitorRegistration(); await test.step('Expect registerGuest to create guest 1', async () => { await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); @@ -568,11 +550,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - registerGuest multiple times', async () => { - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; + const registerGuestVisitor = createFakeVisitorRegistration(); await test.step('Expect registerGuest work with the same token, multiple times', async () => { await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); @@ -611,11 +589,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - setGuestEmail', async () => { - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; + const registerGuestVisitor = createFakeVisitorRegistration(); // Start Chat await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).toBeVisible(); @@ -649,11 +623,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - setGuestName', async () => { - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; + const registerGuestVisitor = createFakeVisitorRegistration(); // Start Chat await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).toBeVisible(); @@ -683,11 +653,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - setGuestToken', async ({ browser }) => { - const registerGuestVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - token: faker.string.uuid(), - }; + const registerGuestVisitor = createFakeVisitorRegistration(); // Register guest and send a message await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); @@ -731,7 +697,7 @@ test.describe('OC - Livechat API', () => { test.beforeAll(async ({ api }) => { agent = await createAgent(api, 'user1'); - await expect((await api.post('/settings/Livechat_offline_email', { value: 'test@testing.com' })).status()).toBe(200); + expect((await api.post('/settings/Livechat_offline_email', { value: 'test@testing.com' })).status()).toBe(200); }); test.beforeEach(async ({ browser }, testInfo) => { @@ -790,10 +756,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - onChatStarted & onChatEnded', async () => { - const newVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + const newVisitor = createFakeVisitor(); await test.step('Expect onChatStarted to trigger callback', async () => { const watchForTrigger = page.waitForFunction(() => window.onChatStarted === true); @@ -832,10 +795,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - onPrechatFormSubmit & onAssignAgent', async () => { - const newVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + const newVisitor = createFakeVisitor(); await test.step('Expect onPrechatFormSubmit to trigger callback', async () => { const watchForTrigger = page.waitForFunction(() => window.onPrechatFormSubmit === true); @@ -871,10 +831,7 @@ test.describe('OC - Livechat API', () => { // TODO: Fix this Flaky test test.skip('onAgentStatusChange', async () => { - const newVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + const newVisitor = createFakeVisitor(); await poLiveChat.openLiveChat(); await poLiveChat.sendMessage(newVisitor, false); @@ -896,10 +853,7 @@ test.describe('OC - Livechat API', () => { }); test('OC - Livechat API - onOfflineFormSubmit', async () => { - const newVisitor = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + const newVisitor = createFakeVisitor(); await poAuxContext.poHomeOmnichannel.sidenav.switchStatus('offline'); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts index e26c5548d403f..7729b5684228b 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel, OmnichannelLiveChatEmbedded } from '../page-objects'; @@ -15,11 +15,6 @@ declare const window: Window & { }; }; -const createVisitor = () => ({ - name: `${faker.person.firstName()} ${faker.string.uuid()}`, - email: faker.internet.email(), -}); - test.use({ storageState: Users.user1.state }); test.describe('OC - Livechat - Avatar visibility', async () => { @@ -58,7 +53,7 @@ test.describe('OC - Livechat - Avatar visibility', async () => { }); test('OC - Livechat - Change avatar visibility', async () => { - const visitor = createVisitor(); + const visitor = createFakeVisitor(); await test.step('should initiate Livechat conversation', async () => { await poLiveChat.openLiveChat(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts index 4dd3e7e872570..b7a058e719ee1 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts @@ -1,5 +1,4 @@ -import { faker } from '@faker-js/faker'; - +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChatEmbedded } from '../page-objects'; @@ -14,11 +13,6 @@ declare const window: Window & { }; }; -const createVisitor = () => ({ - name: `${faker.person.firstName()} ${faker.string.uuid()}`, - email: faker.internet.email(), -}); - test.use({ storageState: Users.admin.state }); test.skip(!IS_EE, 'Enterprise Only'); @@ -52,7 +46,7 @@ test.describe('OC - Livechat - Message list background', async () => { }); test('OC - Livechat - Change message list background', async ({ api, page }) => { - const visitor = createVisitor(); + const visitor = createFakeVisitor(); await test.step('should initiate Livechat conversation', async () => { await poLiveChat.openLiveChat(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts index 21ffb82db1c72..0c72c8894cc40 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts @@ -1,5 +1,4 @@ -import { faker } from '@faker-js/faker'; - +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -62,10 +61,7 @@ test.describe('OC - Livechat - Department Flow', () => { }); test('OC - Livechat - Chat with Department', async () => { - const guest = { - name: `${faker.person.firstName()} ${faker.string.nanoid(10)}}`, - email: faker.internet.email(), - }; + const guest = createFakeVisitor(); await test.step('expect start Chat with department', async () => { await poLiveChat.openAnyLiveChat(); @@ -89,10 +85,7 @@ test.describe('OC - Livechat - Department Flow', () => { }); test('OC - Livechat - Change Department', async () => { - const guest = { - name: `${faker.person.firstName()} ${faker.string.nanoid(10)}}`, - email: faker.internet.email(), - }; + const guest = createFakeVisitor(); await test.step('expect start Chat with department', async () => { await poLiveChat.openAnyLiveChat(); await poLiveChat.sendMessage(guest, false, departmentA.name); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts index 2eed3c1421267..d96aa4006bc4b 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts @@ -1,15 +1,11 @@ -import { faker } from '@faker-js/faker'; - +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; import { createAgent } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; -const visitor = { - name: `${faker.person.firstName()} ${faker.string.uuid()}}`, - email: faker.internet.email(), -}; +const visitor = createFakeVisitor(); // Endpoint defaults are reset after each test, so if not in matrix assume is true const endpointMatrix = [ diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts index bc167a2994948..70d0441db5e13 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel, OmnichannelLiveChatEmbedded } from '../page-objects'; @@ -15,11 +15,6 @@ declare const window: Window & { }; }; -const createVisitor = () => ({ - name: `${faker.person.firstName()} ${faker.string.uuid()}`, - email: faker.internet.email(), -}); - test.use({ storageState: Users.user1.state }); test.describe('OC - Livechat - Bubble background color', async () => { @@ -58,7 +53,7 @@ test.describe('OC - Livechat - Bubble background color', async () => { }); test('OC - Livechat - Change bubble background color', async () => { - const visitor = createVisitor(); + const visitor = createFakeVisitor(); await test.step('should initiate Livechat conversation', async () => { await poLiveChat.openLiveChat(); @@ -74,8 +69,8 @@ test.describe('OC - Livechat - Bubble background color', async () => { }); await test.step('expect to have default bubble background color', async () => { - await expect(await poLiveChat.messageBubbleBackground('message_from_user')).toBe('rgb(193, 39, 45)'); - await expect(await poLiveChat.messageBubbleBackground('message_from_agent')).toBe('rgb(247, 248, 250)'); + expect(await poLiveChat.messageBubbleBackground('message_from_user')).toBe('rgb(193, 39, 45)'); + expect(await poLiveChat.messageBubbleBackground('message_from_agent')).toBe('rgb(247, 248, 250)'); }); await test.step('expect to change bubble background color', async () => { @@ -86,8 +81,8 @@ test.describe('OC - Livechat - Bubble background color', async () => { }), ); - await expect(await poLiveChat.messageBubbleBackground('message_from_user')).toBe('rgb(186, 218, 85)'); - await expect(await poLiveChat.messageBubbleBackground('message_from_agent')).toBe('rgb(0, 100, 250)'); + expect(await poLiveChat.messageBubbleBackground('message_from_user')).toBe('rgb(186, 218, 85)'); + expect(await poLiveChat.messageBubbleBackground('message_from_agent')).toBe('rgb(0, 100, 250)'); }); await test.step('expect to reset bubble background color to defaults', async () => { @@ -95,8 +90,8 @@ test.describe('OC - Livechat - Bubble background color', async () => { window.RocketChat.livechat.setTheme({ guestBubbleBackgroundColor: undefined, agentBubbleBackgroundColor: undefined }), ); - await expect(await poLiveChat.messageBubbleBackground('message_from_user')).toBe('rgb(193, 39, 45)'); - await expect(await poLiveChat.messageBubbleBackground('message_from_agent')).toBe('rgb(247, 248, 250)'); + expect(await poLiveChat.messageBubbleBackground('message_from_user')).toBe('rgb(193, 39, 45)'); + expect(await poLiveChat.messageBubbleBackground('message_from_agent')).toBe('rgb(247, 248, 250)'); }); await test.step('should close the conversation', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts index 386993a9b6a5c..59d42dfe1e108 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts @@ -1,5 +1,4 @@ -import { faker } from '@faker-js/faker'; - +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; @@ -43,15 +42,12 @@ test.describe('OC - Livechat - Cross Tab Communication', () => { }); test('OC - Livechat - Send messages, close chat and start again 2 tabs', async () => { - const firstUser = { - name: `${faker.person.firstName()} ${faker.string.uuid()}}`, - email: faker.internet.email(), - }; + const visitor = createFakeVisitor(); await test.step('expect livechat conversations to be synced', async () => { await pageLivechat1.openAnyLiveChat(); - await pageLivechat1.sendMessage(firstUser, false); + await pageLivechat1.sendMessage(visitor, false); await pageLivechat1.onlineAgentMessage.fill('this_a_test_message_from_user'); await pageLivechat1.btnSendMessageToOnlineAgent.click(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts index fc8be5b69c814..09883da3c16b6 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts @@ -1,5 +1,4 @@ -import { faker } from '@faker-js/faker'; - +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -7,10 +6,7 @@ import { OmnichannelLiveChat, OmnichannelSettings } from '../page-objects'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; -const visitor = { - name: `${faker.person.firstName()} ${faker.string.uuid()}}`, - email: faker.internet.email(), -}; +const visitor = createFakeVisitor(); test.skip(!IS_EE, 'Enterprise Only'); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts index 24110a15eb257..bf14584ed89fa 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts @@ -1,20 +1,13 @@ -import { faker } from '@faker-js/faker'; - +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; import { createAgent } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; -const firstUser = { - name: `${faker.person.firstName()} ${faker.string.uuid()}}`, - email: faker.internet.email(), -}; +const firstVisitor = createFakeVisitor(); -const secondUser = { - name: `${faker.person.firstName()} ${faker.string.uuid()}}`, - email: faker.internet.email(), -}; +const secondVisitor = createFakeVisitor(); test.use({ storageState: Users.user1.state }); @@ -24,7 +17,7 @@ test.describe.serial('OC - Livechat', () => { test.beforeAll(async ({ api }) => { const statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); }); test.beforeAll(async ({ browser, api }) => { @@ -49,7 +42,7 @@ test.describe.serial('OC - Livechat', () => { await test.step('expect message to be sent by livechat', async () => { await poLiveChat.page.reload(); await poLiveChat.openAnyLiveChat(); - await poLiveChat.sendMessage(firstUser, false); + await poLiveChat.sendMessage(firstVisitor, false); await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_user'); await poLiveChat.btnSendMessageToOnlineAgent.click(); @@ -58,14 +51,14 @@ test.describe.serial('OC - Livechat', () => { }); await test.step('expect message to be received by agent', async () => { - await poHomeOmnichannel.sidenav.openChat(firstUser.name); + await poHomeOmnichannel.sidenav.openChat(firstVisitor.name); await expect(poHomeOmnichannel.content.lastUserMessage).toBeVisible(); await expect(poHomeOmnichannel.content.lastUserMessage).toContainText('this_a_test_message_from_user'); }); }); test('OC - Livechat - Send message to livechat costumer', async () => { - await poHomeOmnichannel.sidenav.openChat(firstUser.name); + await poHomeOmnichannel.sidenav.openChat(firstVisitor.name); await test.step('expect message to be sent by agent', async () => { await poHomeOmnichannel.content.sendMessage('this_a_test_message_from_agent'); @@ -89,7 +82,7 @@ test.describe.serial('OC - Livechat', () => { }); test('OC - Livechat - Close livechat conversation', async () => { - await poHomeOmnichannel.sidenav.openChat(firstUser.name); + await poHomeOmnichannel.sidenav.openChat(firstVisitor.name); await test.step('expect livechat conversation to be closed by agent', async () => { await poHomeOmnichannel.content.btnCloseChat.click(); @@ -106,7 +99,7 @@ test.describe.serial('OC - Livechat - Resub after close room', () => { test.beforeAll(async ({ api }) => { const statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); }); test.beforeAll(async ({ browser, api }) => { @@ -117,7 +110,7 @@ test.describe.serial('OC - Livechat - Resub after close room', () => { const { page: livechatPage } = await createAuxContext(browser, Users.user1, '/livechat', false); poLiveChat = new OmnichannelLiveChat(livechatPage, api); - await poLiveChat.sendMessageAndCloseChat(firstUser); + await poLiveChat.sendMessageAndCloseChat(firstVisitor); }); test.afterAll(async ({ api }) => { @@ -130,14 +123,14 @@ test.describe.serial('OC - Livechat - Resub after close room', () => { test('OC - Livechat - Resub after close room', async () => { await test.step('expect livechat conversation to be opened again, different guest', async () => { await poLiveChat.startNewChat(); - await poLiveChat.sendMessage(secondUser, false); + await poLiveChat.sendMessage(secondVisitor, false); await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_user'); await poLiveChat.btnSendMessageToOnlineAgent.click(); await expect(poLiveChat.page.locator('div >> text="this_a_test_message_from_user"')).toBeVisible(); }); await test.step('expect message to be received by agent', async () => { - await poHomeOmnichannel.sidenav.openChat(secondUser.name); + await poHomeOmnichannel.sidenav.openChat(secondVisitor.name); await expect(poHomeOmnichannel.content.lastUserMessage).toBeVisible(); await expect(poHomeOmnichannel.content.lastUserMessage).toContainText('this_a_test_message_from_user'); }); @@ -155,7 +148,7 @@ test.describe('OC - Livechat - Resume chat after closing', () => { test.beforeAll(async ({ api }) => { const statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); - await expect(statusCode).toBe(200); + expect(statusCode).toBe(200); }); test.beforeAll(async ({ browser, api }) => { @@ -165,7 +158,7 @@ test.describe('OC - Livechat - Resume chat after closing', () => { const { page: livechatPage } = await createAuxContext(browser, Users.user1, '/livechat', false); poLiveChat = new OmnichannelLiveChat(livechatPage, api); - await poLiveChat.sendMessageAndCloseChat(firstUser); + await poLiveChat.sendMessageAndCloseChat(firstVisitor); }); test.afterAll(async ({ api }) => { @@ -184,7 +177,7 @@ test.describe('OC - Livechat - Resume chat after closing', () => { }); await test.step('expect message to be received by agent', async () => { - await poHomeOmnichannel.sidenav.openChat(firstUser.name); + await poHomeOmnichannel.sidenav.openChat(firstVisitor.name); await expect(poHomeOmnichannel.content.lastUserMessage).toBeVisible(); await expect(poHomeOmnichannel.content.lastUserMessage).toContainText('this_a_test_message_from_user'); }); @@ -221,7 +214,7 @@ test.describe('OC - Livechat - Close chat using widget', () => { test('OC - Livechat - Close Chat', async () => { await poLiveChat.openAnyLiveChat(); - await poLiveChat.sendMessage(firstUser, false); + await poLiveChat.sendMessage(firstVisitor, false); await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_user'); await poLiveChat.btnSendMessageToOnlineAgent.click(); @@ -239,7 +232,7 @@ test.describe('OC - Livechat - Close chat using widget', () => { }); test('OC - Livechat - Close Chat twice', async () => { - await poLiveChat.sendMessageAndCloseChat(firstUser); + await poLiveChat.sendMessageAndCloseChat(firstVisitor); await poLiveChat.startNewChat(); await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_user'); await poLiveChat.btnSendMessageToOnlineAgent.click(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts index 31add936d3461..10fe89818fe2f 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts @@ -1,5 +1,4 @@ -import { faker } from '@faker-js/faker'; - +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel } from '../page-objects'; @@ -7,10 +6,7 @@ import { OmnichannelRoomInfo } from '../page-objects/omnichannel-room-info'; import { createConversation } from '../utils/omnichannel/rooms'; import { test, expect } from '../utils/test'; -const NEW_USER = { - name: faker.person.firstName(), - email: faker.internet.email(), -}; +const visitor = createFakeVisitor(); const getPrioritySystemMessage = (username: string, priority: string) => `Priority changed: ${username} changed the priority to ${priority}`; @@ -44,7 +40,7 @@ test.describe.serial('OC - Priorities [Sidebar]', () => { }); test.beforeEach(async ({ api }) => { - await createConversation(api, { visitorName: NEW_USER.name }); + await createConversation(api, { visitorName: visitor.name }); }); test.afterAll(async ({ api }) => { @@ -62,21 +58,21 @@ test.describe.serial('OC - Priorities [Sidebar]', () => { await page.emulateMedia({ reducedMotion: 'reduce' }); await test.step('expect to change inquiry priority using sidebar menu', async () => { - await poHomeChannel.sidenav.getSidebarItemByName(NEW_USER.name).click(); + await poHomeChannel.sidenav.getSidebarItemByName(visitor.name).click(); await expect(poHomeChannel.content.btnTakeChat).toBeVisible(); await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); - await poHomeChannel.sidenav.selectPriority(NEW_USER.name, 'Lowest'); + await poHomeChannel.sidenav.selectPriority(visitor.name, 'Lowest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Lowest')}"`).waitFor(); await expect(poRoomInfo.getLabel('Priority')).toBeVisible(); await expect(poRoomInfo.getInfo('Lowest')).toBeVisible(); - await poHomeChannel.sidenav.selectPriority(NEW_USER.name, 'Highest'); + await poHomeChannel.sidenav.selectPriority(visitor.name, 'Highest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Highest')}"`).waitFor(); await expect(poRoomInfo.getInfo('Highest')).toBeVisible(); - await poHomeChannel.sidenav.selectPriority(NEW_USER.name, 'Unprioritized'); + await poHomeChannel.sidenav.selectPriority(visitor.name, 'Unprioritized'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Unprioritized')}"`).waitFor(); await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); await expect(poRoomInfo.getInfo('Unprioritized')).not.toBeVisible(); @@ -89,16 +85,16 @@ test.describe.serial('OC - Priorities [Sidebar]', () => { await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); - await poHomeChannel.sidenav.selectPriority(NEW_USER.name, 'Lowest'); + await poHomeChannel.sidenav.selectPriority(visitor.name, 'Lowest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Lowest')}"`).waitFor(); await expect(poRoomInfo.getLabel('Priority')).toBeVisible(); await expect(poRoomInfo.getInfo('Lowest')).toBeVisible(); - await poHomeChannel.sidenav.selectPriority(NEW_USER.name, 'Highest'); + await poHomeChannel.sidenav.selectPriority(visitor.name, 'Highest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Highest')}"`).waitFor(); await expect(poRoomInfo.getInfo('Highest')).toBeVisible(); - await poHomeChannel.sidenav.selectPriority(NEW_USER.name, 'Unprioritized'); + await poHomeChannel.sidenav.selectPriority(visitor.name, 'Unprioritized'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Unprioritized')}"`).waitFor(); await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); await expect(poRoomInfo.getInfo('Unprioritized')).not.toBeVisible(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts index 323d415505925..a6bd8640749d8 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -11,14 +11,11 @@ test.skip(!IS_EE, 'Export transcript as PDF > Enterprie Only'); test.describe('omnichannel- export chat transcript as PDF', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeChannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); // Set user user 1 as manager and agent await api.post('/livechat/users/agent', { username: 'user1' }); @@ -41,14 +38,14 @@ test.describe('omnichannel- export chat transcript as PDF', () => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { await new Promise((resolve) => setTimeout(resolve, 5000)); - await agent.poHomeChannel.sidenav.openChat(newUser.name); + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to be not able send transcript as PDF', async () => { @@ -76,7 +73,7 @@ test.describe('omnichannel- export chat transcript as PDF', () => { await test.step('Expect to have exported PDF in rocket.cat', async () => { await agent.poHomeChannel.transcript.contactCenter.click(); await agent.poHomeChannel.transcript.contactCenterChats.click(); - await agent.poHomeChannel.transcript.contactCenterSearch.type(newUser.name); + await agent.poHomeChannel.transcript.contactCenterSearch.type(newVisitor.name); await page.waitForTimeout(3000); await agent.poHomeChannel.transcript.firstRow.click(); await agent.poHomeChannel.transcript.viewFullConversation.click(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts index 032be52d14727..bc19259bfe1f3 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; @@ -9,14 +9,11 @@ import { test, expect } from '../utils/test'; test.describe('omnichannel-transcript', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeChannel: HomeChannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); // Set user user 1 as manager and agent await api.post('/livechat/users/agent', { username: 'user1' }); @@ -39,13 +36,13 @@ test.describe('omnichannel-transcript', () => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { - await agent.poHomeChannel.sidenav.openChat(newUser.name); + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); }); await test.step('Expect to be able to send transcript to email', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts index 399c9394740e0..1d8cd4ea882e9 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -46,10 +46,7 @@ test.describe('omnichannel-takeChat', () => { test.beforeEach('start a new livechat chat', async ({ page, api }) => { await agent.poHomeChannel.sidenav.switchStatus('online'); - newVisitor = { - name: `${faker.person.firstName()} ${faker.string.uuid()}`, - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); poLiveChat = new OmnichannelLiveChat(page, api); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts index 70d2242237445..16253dee3e919 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts @@ -1,6 +1,7 @@ import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -10,14 +11,11 @@ test.describe('OC - Livechat New Chat Triggers - After Registration', () => { let triggersName: string; let triggerMessage: string; let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeEach(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); triggersName = faker.string.uuid(); triggerMessage = 'This is a trigger message after guest registration'; @@ -59,7 +57,7 @@ test.describe('OC - Livechat New Chat Triggers - After Registration', () => { test.describe('OC - Livechat New Chat Triggers - After Registration', async () => { await test('expect trigger message after registration', async () => { await poLiveChat.page.goto('/livechat'); - await poLiveChat.sendMessageAndCloseChat(newUser); + await poLiveChat.sendMessageAndCloseChat(newVisitor); await poLiveChat.startNewChat(); await expect(poLiveChat.txtChatMessage(triggerMessage)).toBeVisible(); @@ -71,7 +69,7 @@ test.describe('OC - Livechat New Chat Triggers - After Registration', () => { await test.step('expect trigger message after registration to be visible', async () => { await poLiveChat.page.goto('/livechat'); await poLiveChat.openAnyLiveChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await expect(poLiveChat.txtChatMessage(triggerMessage)).toBeVisible(); }); @@ -106,12 +104,12 @@ test.describe('OC - Livechat New Chat Triggers - After Registration', () => { await test('expect trigger message after registration', async () => { await poLiveChat.page.goto('/livechat'); - await poLiveChat.sendMessageAndCloseChat(newUser); + await poLiveChat.sendMessageAndCloseChat(newVisitor); await expect(poLiveChat.txtChatMessage(triggerMessage)).toBeVisible(); await poLiveChat.startNewChat(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await expect(poLiveChat.txtChatMessage(triggerMessage)).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts index a61508599e808..1045036a926c5 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -9,14 +9,11 @@ import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); test.describe('OC - Livechat Triggers - Open by Visitor', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); const requests = await Promise.all([ api.post('/livechat/users/agent', { username: 'user1' }), @@ -81,7 +78,7 @@ test.describe('OC - Livechat Triggers - Open by Visitor', () => { await expect(poLiveChat.txtChatMessage('This is a trigger message open by visitor')).toBeVisible(); await poLiveChat.btnChatNow.click(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts index f52e1ca13709a..66bf1058ebfa6 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts @@ -1,6 +1,6 @@ -import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -9,14 +9,11 @@ import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); test.describe('OC - Livechat Triggers - Time on site', () => { let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); const requests = await Promise.all([ api.post('/livechat/users/agent', { username: 'user1' }), @@ -77,7 +74,7 @@ test.describe('OC - Livechat Triggers - Time on site', () => { await poLiveChat.btnOpenOnlineLiveChat('Start chat').click(); await poLiveChat.btnOpenOnlineLiveChat('Chat now').click(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); await test.step('expect to not have any trigger message after registration', async () => { await expect(poLiveChat.txtChatMessage('This is a trigger message time on site')).not.toBeVisible(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts index 78d41c7dfd6be..8add05c7c1e68 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts @@ -1,6 +1,7 @@ import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; +import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; @@ -10,14 +11,11 @@ test.describe.serial('OC - Livechat Triggers', () => { let triggersName: string; let triggerMessage: string; let poLiveChat: OmnichannelLiveChat; - let newUser: { email: string; name: string }; + let newVisitor: { email: string; name: string }; let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { - newUser = { - name: faker.person.firstName(), - email: faker.internet.email(), - }; + newVisitor = createFakeVisitor(); triggersName = faker.string.uuid(); triggerMessage = 'This is a trigger message'; const requests = await Promise.all([ @@ -57,7 +55,7 @@ test.describe.serial('OC - Livechat Triggers', () => { await test.step('expect to register visitor', async () => { await expect(poLiveChat.btnChatNow).not.toBeVisible(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); }); await test.step('expect send a message as a visitor', async () => { @@ -99,7 +97,7 @@ test.describe.serial('OC - Livechat Triggers', () => { await test.step('expect to register visitor', async () => { await poLiveChat.btnChatNow.click(); - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); }); await test.step('expect trigger message after registration', async () => { @@ -139,7 +137,7 @@ test.describe.serial('OC - Livechat Triggers', () => { }); await test.step('expect to register visitor', async () => { - await poLiveChat.sendMessage(newUser, false); + await poLiveChat.sendMessage(newVisitor, false); }); await test.step('expect trigger message after registration', async () => { diff --git a/apps/meteor/tests/mocks/data.ts b/apps/meteor/tests/mocks/data.ts index 5e48d89f7199c..14f03a6bd9ab6 100644 --- a/apps/meteor/tests/mocks/data.ts +++ b/apps/meteor/tests/mocks/data.ts @@ -255,3 +255,22 @@ export function createFakeMessageWithAttachment(overrides?: Partial): ...overrides, }; } + +const guestNames = faker.helpers.uniqueArray(faker.person.firstName, 1000); + +function pullNextVisitorName() { + const guestName = guestNames.pop(); + + if (!guestName) { + throw new Error('exhausted guest names'); + } + + return guestName; +} + +export function createFakeVisitor() { + return { + name: pullNextVisitorName(), + email: faker.internet.email(), + } as const; +} From 6c45b7326df90b1ce0d42c0169cde8af1272320c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 19 Jul 2024 11:58:12 -0300 Subject: [PATCH 062/114] test: closing remaining opened pages (#32844) --- apps/meteor/tests/e2e/image-gallery.spec.ts | 1 + apps/meteor/tests/e2e/messaging.spec.ts | 52 +++++++++++-------- .../omnichannel/omnichannel-takeChat.spec.ts | 9 ++-- .../tests/e2e/page-objects/home-channel.ts | 2 +- apps/meteor/tests/e2e/read-receipts.spec.ts | 14 ++++- .../meteor/tests/e2e/retention-policy.spec.ts | 20 ++++--- .../tests/e2e/video-conference-ring.spec.ts | 16 ++++-- 7 files changed, 73 insertions(+), 41 deletions(-) diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index 0291eefff9dfb..526e695870df6 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -31,6 +31,7 @@ test.describe.serial('Image Gallery', async () => { }); test.afterAll(async ({ api }) => { + await poHomeChannel.page.close(); await deleteChannel(api, targetChannel); await deleteChannel(api, targetChannelLargeImage); }); diff --git a/apps/meteor/tests/e2e/messaging.spec.ts b/apps/meteor/tests/e2e/messaging.spec.ts index c5b004faeb851..9cd0c996993ee 100644 --- a/apps/meteor/tests/e2e/messaging.spec.ts +++ b/apps/meteor/tests/e2e/messaging.spec.ts @@ -1,3 +1,5 @@ +import type { Page } from '@playwright/test'; + import { createAuxContext } from './fixtures/createAuxContext'; import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; @@ -137,36 +139,40 @@ test.describe.serial('Messaging', () => { await expect(page.locator('[data-qa-type="message"]').last()).toBeFocused(); }); - test('expect show "hello word" in both contexts (targetChannel)', async ({ browser }) => { - await poHomeChannel.sidenav.openChat(targetChannel); - const { page } = await createAuxContext(browser, Users.user2); - const auxContext = { page, poHomeChannel: new HomeChannel(page) }; + test.describe('Both contexts', () => { + let auxContext: { page: Page; poHomeChannel: HomeChannel }; + test.beforeEach(async ({ browser }) => { + const { page } = await createAuxContext(browser, Users.user2); + auxContext = { page, poHomeChannel: new HomeChannel(page) }; + }); - await auxContext.poHomeChannel.sidenav.openChat(targetChannel); + test.afterEach(async () => { + await auxContext.page.close(); + }); - await poHomeChannel.content.sendMessage('hello world'); + test('expect show "hello word" in both contexts (targetChannel)', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); - await expect(async () => { - await expect(auxContext.poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); - await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); - }).toPass(); + await auxContext.poHomeChannel.sidenav.openChat(targetChannel); - await auxContext.page.close(); - }); + await poHomeChannel.content.sendMessage('hello world'); - test('expect show "hello word" in both contexts (direct)', async ({ browser }) => { - await poHomeChannel.sidenav.openChat('user2'); - const { page } = await createAuxContext(browser, Users.user2); - const auxContext = { page, poHomeChannel: new HomeChannel(page) }; - await auxContext.poHomeChannel.sidenav.openChat('user1'); + await expect(async () => { + await expect(auxContext.poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); + }).toPass(); + }); - await poHomeChannel.content.sendMessage('hello world'); + test('expect show "hello word" in both contexts (direct)', async () => { + await poHomeChannel.sidenav.openChat('user2'); + await auxContext.poHomeChannel.sidenav.openChat('user1'); - await expect(async () => { - await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); - await expect(auxContext.poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); - }).toPass(); + await poHomeChannel.content.sendMessage('hello world'); - await auxContext.page.close(); + await expect(async () => { + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); + await expect(auxContext.poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); + }).toPass(); + }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts index 1d8cd4ea882e9..92a06ffb061a7 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts @@ -34,13 +34,12 @@ test.describe('omnichannel-takeChat', () => { await agent.poHomeChannel.sidenav.switchOmnichannelStatus('online'); await agent.poHomeChannel.sidenav.switchStatus('online'); + await agent.page.close(); await Promise.all([ - await api.delete('/livechat/users/agent/user1').then((res) => expect(res.status()).toBe(200)), - await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }).then((res) => expect(res.status()).toBe(200)), - await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true }).then((res) => expect(res.status()).toBe(200)), + await api.delete('/livechat/users/agent/user1'), + await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }), + await api.post('/settings/Livechat_enabled_when_agent_idle', { value: true }), ]); - - await agent.page.close(); }); test.beforeEach('start a new livechat chat', async ({ page, api }) => { diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index a5c9fa478c6c5..7b3c0d093ddc3 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -4,7 +4,7 @@ import { expect } from '../utils/test'; import { HomeContent, HomeSidenav, HomeFlextab } from './fragments'; export class HomeChannel { - private readonly page: Page; + public readonly page: Page; readonly content: HomeContent; diff --git a/apps/meteor/tests/e2e/read-receipts.spec.ts b/apps/meteor/tests/e2e/read-receipts.spec.ts index 54eb52f06fa68..f61e1b2e1d1f7 100644 --- a/apps/meteor/tests/e2e/read-receipts.spec.ts +++ b/apps/meteor/tests/e2e/read-receipts.spec.ts @@ -1,3 +1,5 @@ +import type { Page } from '@playwright/test'; + import { IS_EE } from './config/constants'; import { createAuxContext } from './fixtures/createAuxContext'; import { Users } from './fixtures/userStates'; @@ -43,14 +45,22 @@ test.describe.serial('read-receipts', () => { await setSettingValueById(api, 'Message_Read_Receipt_Store_Users', false); }); + let auxContext: { page: Page; poHomeChannel: HomeChannel } | undefined; + + test.afterEach(async () => { + if (auxContext) { + await auxContext.page.close(); + } + auxContext = undefined; + }); + test('should show read receipts message sent status in the sent message', async ({ browser }) => { const { page } = await createAuxContext(browser, Users.user1); - const auxContext = { page, poHomeChannel: new HomeChannel(page) }; + auxContext = { page, poHomeChannel: new HomeChannel(page) }; await auxContext.poHomeChannel.sidenav.openChat(targetChannel); await auxContext.poHomeChannel.content.sendMessage('hello admin'); await expect(auxContext.poHomeChannel.content.lastUserMessage.getByRole('status', { name: 'Message sent' })).toBeVisible(); - await auxContext.page.close(); }); test('should show read receipts message viewed status in the sent message', async () => { diff --git a/apps/meteor/tests/e2e/retention-policy.spec.ts b/apps/meteor/tests/e2e/retention-policy.spec.ts index 7464fd736a976..708e5f19b5200 100644 --- a/apps/meteor/tests/e2e/retention-policy.spec.ts +++ b/apps/meteor/tests/e2e/retention-policy.spec.ts @@ -1,4 +1,5 @@ import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; import { timeUnitToMs, TIMEUNIT } from '../../client/lib/convertTimeUnit'; import { createAuxContext } from './fixtures/createAuxContext'; @@ -91,20 +92,26 @@ test.describe.serial('retention-policy', () => { }); test.describe('edit-room-retention-policy permission', async () => { - test('should not show prune section in edit channel for users without permission', async ({ browser }) => { + let auxContext: { page: Page; poHomeChannel: HomeChannel }; + test.beforeEach(async ({ browser }) => { const { page } = await createAuxContext(browser, Users.user1); - const auxContext = { page, poHomeChannel: new HomeChannel(page) }; + auxContext = { page, poHomeChannel: new HomeChannel(page) }; + await auxContext.poHomeChannel.sidenav.openChat(targetChannel); + await auxContext.poHomeChannel.tabs.btnRoomInfo.click(); + await auxContext.poHomeChannel.tabs.room.btnEdit.click(); + }); + test.afterEach(async () => { + await auxContext.page.close(); + }); + test('should not show prune section in edit channel for users without permission', async () => { await auxContext.poHomeChannel.sidenav.openChat(targetChannel); await auxContext.poHomeChannel.tabs.btnRoomInfo.click(); await auxContext.poHomeChannel.tabs.room.btnEdit.click(); await expect(poHomeChannel.tabs.room.pruneAccordion).not.toBeVisible(); - await auxContext.page.close(); }); - test('users without permission should be able to edit the channel', async ({ browser }) => { - const { page } = await createAuxContext(browser, Users.user1); - const auxContext = { page, poHomeChannel: new HomeChannel(page) }; + test('users without permission should be able to edit the channel', async () => { await auxContext.poHomeChannel.sidenav.openChat(targetChannel); await auxContext.poHomeChannel.tabs.btnRoomInfo.click(); await auxContext.poHomeChannel.tabs.room.btnEdit.click(); @@ -112,7 +119,6 @@ test.describe.serial('retention-policy', () => { await auxContext.poHomeChannel.tabs.room.btnSave.click(); await expect(auxContext.poHomeChannel.getSystemMessageByText('set room to read only')).toBeVisible(); - await auxContext.page.close(); }); }); diff --git a/apps/meteor/tests/e2e/video-conference-ring.spec.ts b/apps/meteor/tests/e2e/video-conference-ring.spec.ts index 03ab7bd53c33d..3c6ba1730e3d4 100644 --- a/apps/meteor/tests/e2e/video-conference-ring.spec.ts +++ b/apps/meteor/tests/e2e/video-conference-ring.spec.ts @@ -1,3 +1,5 @@ +import type { Page } from '@playwright/test'; + import { IS_EE } from './config/constants'; import { createAuxContext } from './fixtures/createAuxContext'; import { Users } from './fixtures/userStates'; @@ -17,10 +19,18 @@ test.describe('video conference ringing', () => { await page.goto('/home'); }); - test('expect is ringing in direct', async ({ browser }) => { - await poHomeChannel.sidenav.openChat('user2'); + let auxContext: { page: Page; poHomeChannel: HomeChannel }; + test.beforeEach(async ({ browser }) => { const { page } = await createAuxContext(browser, Users.user2); - const auxContext = { page, poHomeChannel: new HomeChannel(page) }; + auxContext = { page, poHomeChannel: new HomeChannel(page) }; + }); + + test.afterEach(async () => { + await auxContext.page.close(); + }); + + test('expect is ringing in direct', async () => { + await poHomeChannel.sidenav.openChat('user2'); await auxContext.poHomeChannel.sidenav.openChat('user1'); await poHomeChannel.content.btnCall.click(); From 47819cd427ffb67970c48fdc8d519ac7cd2e4942 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Fri, 19 Jul 2024 21:42:29 +0530 Subject: [PATCH 063/114] revert: pr 32541 (#32814) This reverts commit 06707d86e14b6ac3122782b3e6ba6ef1ff8cd449. --- .changeset/cuddly-ravens-swim.md | 5 ----- .../app/settings/server/SettingsRegistry.ts | 22 +++++++------------ 2 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 .changeset/cuddly-ravens-swim.md diff --git a/.changeset/cuddly-ravens-swim.md b/.changeset/cuddly-ravens-swim.md deleted file mode 100644 index 5774ef48202d0..0000000000000 --- a/.changeset/cuddly-ravens-swim.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@rocket.chat/meteor": patch ---- - -Fixed an issue where adding `OVERWRITE_SETTING_` for any setting wasn't immediately taking effect sometimes, and needed a server restart to reflect. diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts index 443e38ce5d63e..5783e2946dc1f 100644 --- a/apps/meteor/app/settings/server/SettingsRegistry.ts +++ b/apps/meteor/app/settings/server/SettingsRegistry.ts @@ -138,22 +138,17 @@ export class SettingsRegistry { const settingFromCodeOverwritten = overwriteSetting(settingFromCode); - const settingOverwrittenDefault = overrideSetting(settingFromCode); - const settingStored = this.store.getSetting(_id); - const settingStoredOverwritten = settingStored && overwriteSetting(settingStored); - const isOverwritten = settingFromCode !== settingFromCodeOverwritten || (settingStored && settingStored !== settingStoredOverwritten); - - const updatedSettingAfterApplyingOverwrite = isOverwritten ? settingFromCodeOverwritten : settingOverwrittenDefault; - try { validateSetting(settingFromCode._id, settingFromCode.type, settingFromCode.value); } catch (e) { IS_DEVELOPMENT && SystemLogger.error(`Invalid setting code ${_id}: ${(e as Error).message}`); } + const isOverwritten = settingFromCode !== settingFromCodeOverwritten || (settingStored && settingStored !== settingStoredOverwritten); + const { _id: _, ...settingProps } = settingFromCodeOverwritten; if (settingStored && !compareSettings(settingStored, settingFromCodeOverwritten)) { @@ -171,9 +166,6 @@ export class SettingsRegistry { })(); await this.saveUpdatedSetting(_id, updatedProps, removedKeys); - - this.store.set(updatedSettingAfterApplyingOverwrite); - return; } @@ -183,8 +175,6 @@ export class SettingsRegistry { const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key)); await this.saveUpdatedSetting(_id, settingProps, removedKeys); - - this.store.set(updatedSettingAfterApplyingOverwrite); } return; } @@ -198,9 +188,13 @@ export class SettingsRegistry { return; } - await this.model.insertOne(updatedSettingAfterApplyingOverwrite); // no need to emit unless we remove the oplog + const settingOverwrittenDefault = overrideSetting(settingFromCode); + + const setting = isOverwritten ? settingFromCodeOverwritten : settingOverwrittenDefault; + + await this.model.insertOne(setting); // no need to emit unless we remove the oplog - this.store.set(updatedSettingAfterApplyingOverwrite); + this.store.set(setting); } /* From d3a6299cbd1e0e24f36d846fa17f632e3f5b0877 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:28:49 +0530 Subject: [PATCH 064/114] feat(Omnichannel): Attachments in Email Transcripts (#32777) --- .changeset/weak-taxis-design.md | 5 ++ .../app/livechat/server/lib/LivechatTyped.ts | 65 +++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 .changeset/weak-taxis-design.md diff --git a/.changeset/weak-taxis-design.md b/.changeset/weak-taxis-design.md new file mode 100644 index 0000000000000..a2d435495cd71 --- /dev/null +++ b/.changeset/weak-taxis-design.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Added handling of attachments in Omnichannel email transcripts. Earlier attachments were being skipped and were being shown as empty space, now it should render the image attachments and should show relevant error message for unsupported attachments. diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index bb56eb81ceb31..800e32d85ab4b 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -23,7 +23,8 @@ import type { LivechatDepartmentDTO, OmnichannelSourceType, } from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus, UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, UserStatus, isFileAttachment, isFileImageAttachment, isOmnichannelRoom } from '@rocket.chat/core-typings'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; import { Logger, type MainLogger } from '@rocket.chat/logger'; import { LivechatDepartment, @@ -37,6 +38,7 @@ import { ReadReceipts, Rooms, LivechatCustomField, + Uploads, } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; @@ -613,6 +615,7 @@ class LivechatClass { 'livechat-started', 'livechat_video_call', ]; + const acceptableImageMimeTypes = ['image/jpeg', 'image/png', 'image/jpg']; const messages = await Messages.findVisibleByRoomIdNotContainingTypesBeforeTs( rid, ignoredMessageTypes, @@ -623,7 +626,14 @@ class LivechatClass { ); let html = '

'; - await messages.forEach((message) => { + const InvalidFileMessage = `
${i18n.t( + 'This_attachment_is_not_supported', + { lng: userLanguage }, + )}
`; + + for await (const message of messages) { let author; if (message.u._id === visitor._id) { author = i18n.t('You', { lng: userLanguage }); @@ -631,13 +641,60 @@ class LivechatClass { author = showAgentInfo ? message.u.name || message.u.username : i18n.t('Agent', { lng: userLanguage }); } + let messageContent = message.msg; + let filesHTML = ''; + + if (message.attachments && message.attachments?.length > 0) { + messageContent = message.attachments[0].description || ''; + + for await (const attachment of message.attachments) { + if (!isFileAttachment(attachment)) { + // ignore other types of attachments + continue; + } + + if (!isFileImageAttachment(attachment)) { + filesHTML += `
${attachment.title || ''}${InvalidFileMessage}
`; + continue; + } + + if (!attachment.image_type || !acceptableImageMimeTypes.includes(attachment.image_type)) { + filesHTML += `
${attachment.title || ''}${InvalidFileMessage}
`; + continue; + } + + // Image attachment can be rendered in email body + const file = message.files?.find((file) => file.name === attachment.title); + + if (!file) { + filesHTML += `
${attachment.title || ''}${InvalidFileMessage}
`; + continue; + } + + const uploadedFile = await Uploads.findOneById(file._id); + + if (!uploadedFile) { + filesHTML += `
${file.name}${InvalidFileMessage}
`; + continue; + } + + const uploadedFileBuffer = await FileUpload.getBuffer(uploadedFile); + filesHTML += `

${file.name}

`; + } + } + const datetime = moment.tz(message.ts, timezone).locale(userLanguage).format('LLL'); const singleMessage = `

${author} ${datetime}

-

${message.msg}

+

${messageContent}

+

${filesHTML}

`; html += singleMessage; - }); + } html = `${html}
`; From ebc858fcbe6d2f340835dc76e3e765665bc2e825 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Sat, 20 Jul 2024 00:02:02 +0530 Subject: [PATCH 065/114] fix: Client crashing on firefox private window (#32807) --- .changeset/new-balloons-speak.md | 5 ++++ .../structure/AttachmentDownloadBase.tsx | 5 ++-- .../hooks/useDownloadFromServiceWorker.ts | 23 ++++++++++++------- .../RoomFiles/components/FileItemMenu.tsx | 22 ++++++++++++------ packages/i18n/src/locales/en.i18n.json | 1 + 5 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 .changeset/new-balloons-speak.md diff --git a/.changeset/new-balloons-speak.md b/.changeset/new-balloons-speak.md new file mode 100644 index 0000000000000..7d4e7cd3a57e9 --- /dev/null +++ b/.changeset/new-balloons-speak.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed web client crashing on Firefox private window. Firefox disables access to service workers inside private windows. Rocket.Chat needs service workers to process E2EE encrypted files on rooms. These types of files won't be available inside private windows, but the rest of E2EE encrypted features should work normally diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx index dae6b9024776a..284cb0cecbf20 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentDownloadBase.tsx @@ -6,18 +6,19 @@ import Action from '../../Action'; type AttachmentDownloadBaseProps = Omit, 'icon'> & { title?: string | undefined; href: string }; -const AttachmentDownloadBase = ({ title, href, ...props }: AttachmentDownloadBaseProps) => { +const AttachmentDownloadBase = ({ title, href, disabled, ...props }: AttachmentDownloadBaseProps) => { const t = useTranslation(); return ( ); diff --git a/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts index 5ab7f804fec78..199d1507e2847 100644 --- a/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts +++ b/apps/meteor/client/hooks/useDownloadFromServiceWorker.ts @@ -7,13 +7,15 @@ import { downloadAs } from '../lib/download'; const ee = new Emitter>(); -navigator.serviceWorker.addEventListener('message', (event) => { - if (event.data.type === 'attachment-download-result') { - const { result } = event.data as { result: ArrayBuffer; id: string }; +if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.type === 'attachment-download-result') { + const { result } = event.data as { result: ArrayBuffer; id: string }; - ee.emit(event.data.id, { result, id: event.data.id }); - } -}); + ee.emit(event.data.id, { result, id: event.data.id }); + } + }); +} export const registerDownloadForUid = (uid: string, t: ReturnType['t'], title?: string) => { ee.once(uid, ({ result }) => { @@ -23,8 +25,13 @@ export const registerDownloadForUid = (uid: string, t: ReturnType { if (!controller) { - controller = navigator.serviceWorker.controller; + controller = navigator?.serviceWorker?.controller; + } + + if (!controller) { + return; } + controller?.postMessage({ type: 'attachment-download', url: href, @@ -33,7 +40,7 @@ export const forAttachmentDownload = (uid: string, href: string, controller?: Se }; export const useDownloadFromServiceWorker = (href: string, title?: string) => { - const { controller } = navigator.serviceWorker; + const { controller } = navigator?.serviceWorker || {}; const uid = useUniqueId(); diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx index 157df8d78027c..4fbf2fc477f11 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItemMenu.tsx @@ -17,21 +17,24 @@ type FileItemMenuProps = { const ee = new Emitter>(); -navigator.serviceWorker.addEventListener('message', (event) => { - if (event.data.type === 'attachment-download-result') { - const { result } = event.data as { result: ArrayBuffer; id: string }; +if ('serviceWorker' in navigator) { + navigator.serviceWorker.addEventListener('message', (event) => { + if (event.data.type === 'attachment-download-result') { + const { result } = event.data as { result: ArrayBuffer; id: string }; - ee.emit(event.data.id, { result, id: event.data.id }); - } -}); + ee.emit(event.data.id, { result, id: event.data.id }); + } + }); +} const FileItemMenu = ({ fileData, onClickDelete }: FileItemMenuProps) => { const t = useTranslation(); const room = useRoom(); const userId = useUserId(); const isDeletionAllowed = useMessageDeletionIsAllowed(room._id, fileData, userId); + const canDownloadFile = !fileData.encryption || 'serviceWorker' in navigator; - const { controller } = navigator.serviceWorker; + const { controller } = navigator?.serviceWorker || {}; const uid = useUniqueId(); @@ -53,6 +56,10 @@ const FileItemMenu = ({ fileData, onClickDelete }: FileItemMenuProps) => { ), action: () => { if (fileData.path?.includes('/file-decrypt/')) { + if (!controller) { + return; + } + controller?.postMessage({ type: 'attachment-download', url: fileData.path, @@ -68,6 +75,7 @@ const FileItemMenu = ({ fileData, onClickDelete }: FileItemMenuProps) => { URL.revokeObjectURL(fileData.url); } }, + disabled: !canDownloadFile, }, ...(isDeletionAllowed && onClickDelete && { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 0b569b4ee5644..7be120d633a56 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1757,6 +1757,7 @@ "Dont_ask_me_again_list": "Don't ask me again list", "Download": "Download", "Download_Destkop_App": "Download Desktop App", + "Download_Disabled": "Download disabled", "Download_Info": "Download info", "Download_My_Data": "Download My Data (HTML)", "Download_Pending_Avatars": "Download Pending Avatars", From 4e8aa575a6da8c2961661bc88f7b21ebf3ead0f7 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Fri, 19 Jul 2024 16:36:34 -0300 Subject: [PATCH 066/114] fix: Blocked login when dismissed 2FA modal (#32482) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .changeset/soft-donkeys-thank.md | 8 + .../GenericModal/GenericModal.spec.tsx | 87 +++++++++ .../components/GenericModal/GenericModal.tsx | 35 +++- apps/meteor/client/lib/imperativeModal.tsx | 10 +- apps/meteor/client/portals/ModalPortal.tsx | 30 ++- .../ModalProvider/ModalProvider.spec.tsx | 177 ++++++++++-------- .../providers/ModalProvider/ModalProvider.tsx | 2 +- .../meteor/client/views/modal/ModalRegion.tsx | 9 +- .../hooks/useVideoConfOpenCall.spec.tsx | 11 +- .../actions/useRedirectModerationConsole.ts | 2 +- .../mock-providers/src/MockedModalContext.tsx | 4 +- packages/web-ui-registration/package.json | 3 + 12 files changed, 270 insertions(+), 108 deletions(-) create mode 100644 .changeset/soft-donkeys-thank.md create mode 100644 apps/meteor/client/components/GenericModal/GenericModal.spec.tsx diff --git a/.changeset/soft-donkeys-thank.md b/.changeset/soft-donkeys-thank.md new file mode 100644 index 0000000000000..7273ddcffca48 --- /dev/null +++ b/.changeset/soft-donkeys-thank.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/mock-providers": patch +"@rocket.chat/ui-contexts": patch +"@rocket.chat/web-ui-registration": patch +--- + +Fixed an issue with blocked login when dismissed 2FA modal by clicking outside of it or pressing the escape key diff --git a/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx b/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx new file mode 100644 index 0000000000000..0ef7235729c48 --- /dev/null +++ b/apps/meteor/client/components/GenericModal/GenericModal.spec.tsx @@ -0,0 +1,87 @@ +import { useSetModal } from '@rocket.chat/ui-contexts'; +import { act, screen } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import userEvent from '@testing-library/user-event'; +import type { ReactElement } from 'react'; +import React, { Suspense } from 'react'; + +import ModalProviderWithRegion from '../../providers/ModalProvider/ModalProviderWithRegion'; +import GenericModal from './GenericModal'; + +import '@testing-library/jest-dom'; + +const renderModal = (modalElement: ReactElement) => { + const { + result: { current: setModal }, + } = renderHook(() => useSetModal(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + act(() => { + setModal(modalElement); + }); + + return { setModal }; +}; + +describe('callbacks', () => { + it('should call onClose callback when dismissed', async () => { + const handleClose = jest.fn(); + + renderModal(); + + expect(await screen.findByRole('heading', { name: 'Modal', exact: true })).toBeInTheDocument(); + + userEvent.keyboard('{Escape}'); + + expect(screen.queryByRole('heading', { name: 'Modal', exact: true })).not.toBeInTheDocument(); + + expect(handleClose).toHaveBeenCalled(); + }); + + it('should NOT call onClose callback when confirmed', async () => { + const handleConfirm = jest.fn(); + const handleClose = jest.fn(); + + const { setModal } = renderModal(); + + expect(await screen.findByRole('heading', { name: 'Modal', exact: true })).toBeInTheDocument(); + + userEvent.click(screen.getByRole('button', { name: 'Ok', exact: true })); + + expect(handleConfirm).toHaveBeenCalled(); + + act(() => { + setModal(null); + }); + + expect(screen.queryByRole('heading', { name: 'Modal', exact: true })).not.toBeInTheDocument(); + + expect(handleClose).not.toHaveBeenCalled(); + }); + + it('should NOT call onClose callback when cancelled', async () => { + const handleCancel = jest.fn(); + const handleClose = jest.fn(); + + const { setModal } = renderModal(); + + expect(await screen.findByRole('heading', { name: 'Modal', exact: true })).toBeInTheDocument(); + + userEvent.click(screen.getByRole('button', { name: 'Cancel', exact: true })); + + expect(handleCancel).toHaveBeenCalled(); + + act(() => { + setModal(null); + }); + + expect(screen.queryByRole('heading', { name: 'Modal', exact: true })).not.toBeInTheDocument(); + + expect(handleClose).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/meteor/client/components/GenericModal/GenericModal.tsx b/apps/meteor/client/components/GenericModal/GenericModal.tsx index 914928d4d423d..d371e1ff4ef2c 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.tsx @@ -1,9 +1,9 @@ import { Button, Modal } from '@rocket.chat/fuselage'; -import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { Keys as IconName } from '@rocket.chat/icons'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement, ReactNode, ComponentPropsWithoutRef } from 'react'; -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import type { RequiredModalProps } from './withDoNotAskAgain'; import { withDoNotAskAgain } from './withDoNotAskAgain'; @@ -78,6 +78,31 @@ const GenericModal = ({ const t = useTranslation(); const genericModalId = useUniqueId(); + const dismissedRef = useRef(true); + + const handleConfirm = useEffectEvent(() => { + dismissedRef.current = false; + onConfirm?.(); + }); + + const handleCancel = useEffectEvent(() => { + dismissedRef.current = false; + onCancel?.(); + }); + + const handleCloseButtonClick = useEffectEvent(() => { + dismissedRef.current = true; + onClose?.(); + }); + + useEffect( + () => () => { + if (!dismissedRef.current) return; + onClose?.(); + }, + [onClose], + ); + return ( @@ -86,7 +111,7 @@ const GenericModal = ({ {tagline && {tagline}} {title ?? t('Are_you_sure')} - + {children} @@ -94,7 +119,7 @@ const GenericModal = ({ {annotation && !dontAskAgain && {annotation}} {onCancel && ( - )} @@ -104,7 +129,7 @@ const GenericModal = ({ )} {!wrapperFunction && onConfirm && ( - )} diff --git a/apps/meteor/client/lib/imperativeModal.tsx b/apps/meteor/client/lib/imperativeModal.tsx index 28db6fa107ed8..3740eb1ebc9c3 100644 --- a/apps/meteor/client/lib/imperativeModal.tsx +++ b/apps/meteor/client/lib/imperativeModal.tsx @@ -1,15 +1,15 @@ import { Emitter } from '@rocket.chat/emitter'; import React, { Suspense, createElement } from 'react'; -import type { ComponentProps, ElementType, ReactNode } from 'react'; +import type { ComponentProps, ComponentType, ReactNode } from 'react'; import { modalStore } from '../providers/ModalProvider/ModalStore'; -type ReactModalDescriptor = { +type ReactModalDescriptor = ComponentType> = { component: TComponent; props?: ComponentProps; }; -type ModalDescriptor = ReactModalDescriptor | null; +type ModalDescriptor = ReactModalDescriptor | null; type ModalInstance = { close: () => void; @@ -41,11 +41,11 @@ class ImperativeModalEmmiter extends Emitter<{ update: ModalDescriptor }> { this.store = store; } - open = (descriptor: ReactModalDescriptor): ModalInstance => { + open = >(descriptor: ReactModalDescriptor): ModalInstance => { return this.store.open(mapCurrentModal(descriptor as ModalDescriptor)); }; - push = (descriptor: ReactModalDescriptor): ModalInstance => { + push = >(descriptor: ReactModalDescriptor): ModalInstance => { return this.store.push(mapCurrentModal(descriptor as ModalDescriptor)); }; diff --git a/apps/meteor/client/portals/ModalPortal.tsx b/apps/meteor/client/portals/ModalPortal.tsx index d7c9ae9caa2d8..6b2210d569260 100644 --- a/apps/meteor/client/portals/ModalPortal.tsx +++ b/apps/meteor/client/portals/ModalPortal.tsx @@ -1,18 +1,32 @@ -import type { ReactElement, ReactNode } from 'react'; -import React, { memo, useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { memo } from 'react'; import { createPortal } from 'react-dom'; -import { createAnchor } from '../lib/utils/createAnchor'; -import { deleteAnchor } from '../lib/utils/deleteAnchor'; +const createModalRoot = (): HTMLElement => { + const id = 'modal-root'; + const existing = document.getElementById(id); + + if (existing) return existing; + + const newOne = document.createElement('div'); + newOne.id = id; + document.body.append(newOne); + + return newOne; +}; + +let modalRoot: HTMLElement | null = null; type ModalPortalProps = { children?: ReactNode; }; -const ModalPortal = ({ children }: ModalPortalProps): ReactElement => { - const [modalRoot] = useState(() => createAnchor('modal-root')); - useEffect(() => (): void => deleteAnchor(modalRoot), [modalRoot]); - return <>{createPortal(children, modalRoot)}; +const ModalPortal = ({ children }: ModalPortalProps) => { + if (!modalRoot) { + modalRoot = createModalRoot(); + } + + return createPortal(children, modalRoot); }; export default memo(ModalPortal); diff --git a/apps/meteor/client/providers/ModalProvider/ModalProvider.spec.tsx b/apps/meteor/client/providers/ModalProvider/ModalProvider.spec.tsx index f779333374561..ea062c3248071 100644 --- a/apps/meteor/client/providers/ModalProvider/ModalProvider.spec.tsx +++ b/apps/meteor/client/providers/ModalProvider/ModalProvider.spec.tsx @@ -1,115 +1,138 @@ -// import type { IMessage } from '@rocket.chat/core-typings'; -import { Emitter } from '@rocket.chat/emitter'; import { useSetModal } from '@rocket.chat/ui-contexts'; -import { render, screen } from '@testing-library/react'; -import { expect } from 'chai'; -import type { ReactNode } from 'react'; -import React, { Suspense, createContext, useContext, useEffect } from 'react'; +import { act, render, screen } from '@testing-library/react'; +import type { ForwardedRef, ReactElement } from 'react'; +import React, { Suspense, createContext, createRef, forwardRef, useContext, useImperativeHandle } from 'react'; import GenericModal from '../../components/GenericModal'; import { imperativeModal } from '../../lib/imperativeModal'; import ModalRegion from '../../views/modal/ModalRegion'; import ModalProvider from './ModalProvider'; import ModalProviderWithRegion from './ModalProviderWithRegion'; +import '@testing-library/jest-dom'; -const TestContext = createContext({ title: 'default' }); -const emitter = new Emitter(); +const renderWithSuspense = (ui: ReactElement) => + render(ui, { + wrapper: ({ children }) => {children}, + }); -const TestModal = ({ emitterEvent, modalFunc }: { emitterEvent: string; modalFunc?: () => ReactNode }) => { - const setModal = useSetModal(); - const { title } = useContext(TestContext); +describe('via useSetModal', () => { + const ModalTitleContext = createContext('default'); - useEffect(() => { - emitter.on(emitterEvent, () => { - setModal(modalFunc || undefined}>); - }); - }, [emitterEvent, setModal, title, modalFunc]); + type ModalOpenerAPI = { open: () => void }; - return <>; -}; + const ModalOpener = forwardRef((_: unknown, ref: ForwardedRef) => { + const setModal = useSetModal(); + const title = useContext(ModalTitleContext); + useImperativeHandle(ref, () => ({ + open: () => { + setModal(); + }, + })); + + return null; + }); -describe('Modal Provider', () => { it('should render a modal', async () => { - render( - + const modalOpenerRef = createRef(); + + renderWithSuspense( + + + , + ); + + act(() => { + modalOpenerRef.current?.open(); + }); + + expect(await screen.findByRole('dialog', { name: 'default' })).toBeInTheDocument(); + }); + + it('should render a modal that consumes a context', async () => { + const modalOpenerRef = createRef(); + + renderWithSuspense( + - + - , + , ); - emitter.emit('open'); - expect(await screen.findByText('default')).to.exist; + act(() => { + modalOpenerRef.current?.open(); + }); + + expect(await screen.findByRole('dialog', { name: 'title from context' })).toBeInTheDocument(); }); - it('should render a modal that is passed as a function', async () => { - render( - + it('should render a modal in another region', async () => { + const modalOpener1Ref = createRef(); + const modalOpener2Ref = createRef(); + + renderWithSuspense( + - undefined} />} /> + - , + + + + + + , ); - emitter.emit('open'); - expect(await screen.findByText('function modal')).to.exist; + + act(() => { + modalOpener1Ref.current?.open(); + }); + + expect(await screen.findByRole('dialog', { name: 'modal1' })).toBeInTheDocument(); + + act(() => { + modalOpener2Ref.current?.open(); + }); + + expect(await screen.findByRole('dialog', { name: 'modal2' })).toBeInTheDocument(); }); +}); + +describe('via imperativeModal', () => { + it('should render a modal through imperative modal', async () => { + renderWithSuspense( + + + , + ); - it('should render a modal through imperative modal', () => { - async () => { - render( - - - - - , - ); - - const { close } = imperativeModal.open({ + act(() => { + imperativeModal.open({ component: GenericModal, - props: { title: 'imperativeModal' }, + props: { title: 'imperativeModal', open: true }, }); + }); - expect(await screen.findByText('imperativeModal')).to.exist; + expect(await screen.findByRole('dialog', { name: 'imperativeModal' })).toBeInTheDocument(); - close(); + act(() => { + imperativeModal.close(); + }); - expect(screen.queryByText('imperativeModal')).to.not.exist; - }; + expect(screen.queryByText('imperativeModal')).not.toBeInTheDocument(); }); it('should not render a modal if no corresponding region exists', async () => { // ModalProviderWithRegion will always have a region identifier set // and imperativeModal will only render modals in the default region (e.g no region identifier) - render( - - - , - ); - - imperativeModal.open({ - component: GenericModal, - props: { title: 'imperativeModal' }, - }); - expect(screen.queryByText('imperativeModal')).to.not.exist; - }); + renderWithSuspense(); - it('should render a modal in another region', () => { - render( - - - - - - - - - - , - ); + act(() => { + imperativeModal.open({ + component: GenericModal, + props: { title: 'imperativeModal', open: true }, + }); + }); - emitter.emit('openModal1'); - expect(screen.getByText('modal1')).to.exist; - emitter.emit('openModal2'); - expect(screen.getByText('modal2')).to.exist; + expect(screen.queryByRole('dialog', { name: 'imperativeModal' })).not.toBeInTheDocument(); }); }); diff --git a/apps/meteor/client/providers/ModalProvider/ModalProvider.tsx b/apps/meteor/client/providers/ModalProvider/ModalProvider.tsx index 6c3f1026bc517..27092ea602b6a 100644 --- a/apps/meteor/client/providers/ModalProvider/ModalProvider.tsx +++ b/apps/meteor/client/providers/ModalProvider/ModalProvider.tsx @@ -33,7 +33,7 @@ const ModalProvider = ({ children, region }: ModalProviderProps) => { }, region, }), - [currentModal, region, setModal], + [currentModal?.node, currentModal?.region, region, setModal], ); return ; diff --git a/apps/meteor/client/views/modal/ModalRegion.tsx b/apps/meteor/client/views/modal/ModalRegion.tsx index 5cbad2b52bc11..284c460ee0439 100644 --- a/apps/meteor/client/views/modal/ModalRegion.tsx +++ b/apps/meteor/client/views/modal/ModalRegion.tsx @@ -1,6 +1,7 @@ -import { useModal, useCurrentModal } from '@rocket.chat/ui-contexts'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useCurrentModal, useModal } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { lazy, useCallback } from 'react'; +import React, { lazy } from 'react'; import ModalBackdrop from '../../components/ModalBackdrop'; import ModalPortal from '../../portals/ModalPortal'; @@ -10,7 +11,9 @@ const FocusScope = lazy(() => import('react-aria').then((module) => ({ default: const ModalRegion = (): ReactElement | null => { const currentModal = useCurrentModal(); const { setModal } = useModal(); - const handleDismiss = useCallback(() => setModal(null), [setModal]); + const handleDismiss = useEffectEvent(() => { + setModal(null); + }); if (!currentModal) { return null; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall.spec.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall.spec.tsx index ed14eacb7191f..c64dd19b2dbf0 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall.spec.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall.spec.tsx @@ -1,20 +1,18 @@ import { faker } from '@faker-js/faker'; import { ModalContext } from '@rocket.chat/ui-contexts'; -import type { WrapperComponent } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks'; +import type { ReactNode } from 'react'; import React from 'react'; import { useVideoConfOpenCall } from './useVideoConfOpenCall'; describe('with window.RocketChatDesktop set', () => { - const wrapper: WrapperComponent = ({ children }) => ( + const wrapper = ({ children }: { children: ReactNode }) => ( { - return null; - }, + setModal: () => null, }, currentModal: { component: null }, }} @@ -54,7 +52,8 @@ describe('with window.RocketChatDesktop set', () => { describe('with window.RocketChatDesktop unset', () => { const setModal = jest.fn(); - const wrapper: WrapperComponent = ({ children }) => ( + + const wrapper = ({ children }: { children: ReactNode }) => ( { - const [currentModal, setCurrentModal] = React.useState(null); + const [currentModal, setCurrentModal] = useState(null); return ( Date: Sat, 20 Jul 2024 02:13:40 +0530 Subject: [PATCH 067/114] feat: Apps-Engine method to read multiple messages from a room (#32176) Co-authored-by: Douglas Gubert <1810309+d-gubert@users.noreply.github.com> --- .changeset/polite-foxes-repair.md | 5 ++ apps/meteor/app/apps/server/bridges/rooms.ts | 48 ++++++++++++++----- .../app/apps/server/converters/messages.js | 34 +++++++++++++ packages/apps/src/AppsEngine.ts | 1 + .../src/converters/IAppMessagesConverter.ts | 4 +- yarn.lock | 6 +-- 6 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 .changeset/polite-foxes-repair.md diff --git a/.changeset/polite-foxes-repair.md b/.changeset/polite-foxes-repair.md new file mode 100644 index 0000000000000..2f524c7e5f105 --- /dev/null +++ b/.changeset/polite-foxes-repair.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Added a method to the Apps-Engine that allows apps to read multiple messages from a room diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 86817c5721e2b..344acc74bda43 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -1,11 +1,13 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; -import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IMessage, IMessageRaw } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import type { GetMessagesOptions } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; import { RoomBridge } from '@rocket.chat/apps-engine/server/bridges/RoomBridge'; -import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom } from '@rocket.chat/core-typings'; -import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; +import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as ICoreMessage } from '@rocket.chat/core-typings'; +import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models'; +import type { FindOptions, Sort } from 'mongodb'; import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; import { createDiscussion } from '../../../discussion/server/methods/createDiscussion'; @@ -103,6 +105,38 @@ export class AppRoomBridge extends RoomBridge { return this.orch.getConverters()?.get('users').convertById(room.u._id); } + protected async getMessages(roomId: string, options: GetMessagesOptions, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is getting the messages of the room: "${roomId}" with options:`, options); + + const { limit, skip = 0, sort: _sort } = options; + + const messageConverter = this.orch.getConverters()?.get('messages'); + if (!messageConverter) { + throw new Error('Message converter not found'); + } + + // We support only one field for now + const sort: Sort | undefined = _sort?.createdAt ? { ts: _sort.createdAt } : undefined; + + const messageQueryOptions: FindOptions = { + limit, + skip, + sort, + }; + + const query = { + rid: roomId, + _hidden: { $ne: true }, + t: { $exists: false }, + }; + + const cursor = Messages.find(query, messageQueryOptions); + + const messagePromises: Promise[] = await cursor.map((message) => messageConverter.convertMessageRaw(message)).toArray(); + + return Promise.all(messagePromises); + } + protected async getMembers(roomId: string, appId: string): Promise> { this.orch.debugLog(`The App ${appId} is getting the room's members by room id: "${roomId}"`); const subscriptions = await Subscriptions.findByRoomId(roomId, {}); @@ -220,12 +254,4 @@ export class AppRoomBridge extends RoomBridge { const members = await Users.findUsersByUsernames(usernames, { limit: 50 }).toArray(); await Promise.all(members.map((user) => removeUserFromRoom(roomId, user))); } - - protected getMessages( - _roomId: string, - _options: { limit: number; skip?: number; sort?: Record }, - _appId: string, - ): Promise { - throw new Error('Method not implemented.'); - } } diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 9804e0bb3b2c2..199722a0d8961 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -17,6 +17,40 @@ export class AppMessagesConverter { return this.convertMessage(msg); } + async convertMessageRaw(msgObj) { + if (!msgObj) { + return undefined; + } + + const { attachments, ...message } = msgObj; + const getAttachments = async () => this._convertAttachmentsToApp(attachments); + + const map = { + id: '_id', + threadId: 'tmid', + reactions: 'reactions', + parseUrls: 'parseUrls', + text: 'msg', + createdAt: 'ts', + updatedAt: '_updatedAt', + editedAt: 'editedAt', + emoji: 'emoji', + avatarUrl: 'avatar', + alias: 'alias', + file: 'file', + customFields: 'customFields', + groupable: 'groupable', + token: 'token', + blocks: 'blocks', + roomId: 'rid', + editor: 'editedBy', + attachments: getAttachments, + sender: 'u', + }; + + return transformMappedData(message, map); + } + async convertMessage(msgObj) { if (!msgObj) { return undefined; diff --git a/packages/apps/src/AppsEngine.ts b/packages/apps/src/AppsEngine.ts index 856bc12537905..b85672d23f5a4 100644 --- a/packages/apps/src/AppsEngine.ts +++ b/packages/apps/src/AppsEngine.ts @@ -8,6 +8,7 @@ export type { IVisitorPhone as IAppsVisitorPhone, } from '@rocket.chat/apps-engine/definition/livechat'; export type { IMessage as IAppsMessage } from '@rocket.chat/apps-engine/definition/messages'; +export type { IMessageRaw as IAppsMesssageRaw } from '@rocket.chat/apps-engine/definition/messages'; export { AppInterface as AppEvents } from '@rocket.chat/apps-engine/definition/metadata'; export type { IUser as IAppsUser } from '@rocket.chat/apps-engine/definition/users'; export type { IRole as IAppsRole } from '@rocket.chat/apps-engine/definition/roles'; diff --git a/packages/apps/src/converters/IAppMessagesConverter.ts b/packages/apps/src/converters/IAppMessagesConverter.ts index 185e247895de2..863c10c954777 100644 --- a/packages/apps/src/converters/IAppMessagesConverter.ts +++ b/packages/apps/src/converters/IAppMessagesConverter.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import type { IAppsMessage } from '../AppsEngine'; +import type { IAppsMessage, IAppsMesssageRaw } from '../AppsEngine'; export interface IAppMessagesConverter { convertById(messageId: IMessage['_id']): Promise; @@ -10,4 +10,6 @@ export interface IAppMessagesConverter { convertAppMessage(message: undefined | null): Promise; convertAppMessage(message: IAppsMessage): Promise; convertAppMessage(message: IAppsMessage | undefined | null): Promise; + convertMessageRaw(message: IMessage): Promise; + convertMessageRaw(message: IMessage | undefined | null): Promise; } diff --git a/yarn.lock b/yarn.lock index e7555d99208ed..6d962b0844935 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8510,8 +8510,8 @@ __metadata: linkType: soft "@rocket.chat/apps-engine@npm:alpha": - version: 1.44.0-alpha.818 - resolution: "@rocket.chat/apps-engine@npm:1.44.0-alpha.818" + version: 1.44.0-alpha.828 + resolution: "@rocket.chat/apps-engine@npm:1.44.0-alpha.828" dependencies: "@msgpack/msgpack": 3.0.0-beta2 adm-zip: ^0.5.9 @@ -8527,7 +8527,7 @@ __metadata: uuid: ~8.3.2 peerDependencies: "@rocket.chat/ui-kit": "*" - checksum: acef47bc7f13e0682d018531638b5168c3acd59beae37b013e881ea6fadfe12670fe10545f4a89487f7bedbe9166028833cba7ed3fc401d4283327e47e00e61c + checksum: e26914b62d2e9823577fe8165a2635d65f69ddc315a880bbc417ddd674e4df487dc9bc9507bf3a0616de06cd927596872c1e90e4c29c61da8581e0a1b7c8d97d languageName: node linkType: hard From e6ce4eeb516f0e6f4f0e37a470a041ae8ecd3c3f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 19 Jul 2024 18:28:08 -0300 Subject: [PATCH 068/114] chore(ui-contexts): `useGoToRoom` (#32817) --- .../client/providers/RouterProvider.tsx | 5 ++++ apps/meteor/lib/rooms/coordinator.ts | 5 ++-- .../tests/mocks/client/RouterContextMock.tsx | 1 + packages/core-typings/src/RoomRouteData.ts | 26 +++++++++++++++++ packages/core-typings/src/index.ts | 2 ++ .../src/MockedAppRootBuilder.tsx | 1 + packages/ui-contexts/src/RouterContext.ts | 13 +++++++++ packages/ui-contexts/src/hooks/useGoToRoom.ts | 29 +++++++++++++++++++ packages/ui-contexts/src/index.ts | 1 + 9 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 packages/core-typings/src/RoomRouteData.ts create mode 100644 packages/ui-contexts/src/hooks/useGoToRoom.ts diff --git a/apps/meteor/client/providers/RouterProvider.tsx b/apps/meteor/client/providers/RouterProvider.tsx index 8ba6e699b2b10..d7fb25a1ed31c 100644 --- a/apps/meteor/client/providers/RouterProvider.tsx +++ b/apps/meteor/client/providers/RouterProvider.tsx @@ -1,3 +1,4 @@ +import type { RoomType, RoomRouteData } from '@rocket.chat/core-typings'; import type { RouterContextValue, RouteName, @@ -15,6 +16,7 @@ import type { ReactNode } from 'react'; import React from 'react'; import { appLayout } from '../lib/appLayout'; +import { roomCoordinator } from '../lib/rooms/roomCoordinator'; import { queueMicrotask } from '../lib/utils/queueMicrotask'; const subscribers = new Set<() => void>(); @@ -195,6 +197,9 @@ export const router: RouterContextValue = { defineRoutes, getRoutes, subscribeToRoutesChange, + getRoomRoute(roomType: RoomType, routeData: RoomRouteData) { + return { path: roomCoordinator.getRouteLink(roomType, routeData) || '/' }; + }, }; type RouterProviderProps = { diff --git a/apps/meteor/lib/rooms/coordinator.ts b/apps/meteor/lib/rooms/coordinator.ts index 9cd12d83f21cd..41c4521f4f51d 100644 --- a/apps/meteor/lib/rooms/coordinator.ts +++ b/apps/meteor/lib/rooms/coordinator.ts @@ -1,4 +1,5 @@ import type { RoomType } from '@rocket.chat/core-typings'; +import type { LocationPathname } from '@rocket.chat/ui-contexts'; import type { IRoomTypeConfig, @@ -40,7 +41,7 @@ export abstract class RoomCoordinator { return this.roomTypes[identifier].config; } - public getRouteLink(roomType: string, subData: RoomIdentification): string | false { + public getRouteLink(roomType: string, subData: RoomIdentification): LocationPathname | false { const config = this.getRoomTypeConfig(roomType); if (!config?.route) { return false; @@ -76,7 +77,7 @@ export abstract class RoomCoordinator { // but keep the root slash if it's the only one path = path.match(/^\/{1}$/) ? path : path.replace(/\/$/, ''); - return path; + return path as LocationPathname; } protected getRouteData(roomType: string, subData: RoomIdentification): Record | false { diff --git a/apps/meteor/tests/mocks/client/RouterContextMock.tsx b/apps/meteor/tests/mocks/client/RouterContextMock.tsx index 2831fe226adf8..c81e92dbbdc42 100644 --- a/apps/meteor/tests/mocks/client/RouterContextMock.tsx +++ b/apps/meteor/tests/mocks/client/RouterContextMock.tsx @@ -115,6 +115,7 @@ const RouterContextMock = ({ defineRoutes: () => () => undefined, getRoutes: () => [], subscribeToRoutesChange: () => () => undefined, + getRoomRoute: () => ({ path: '/' }), }; }, [currentPath, navigate])} > diff --git a/packages/core-typings/src/RoomRouteData.ts b/packages/core-typings/src/RoomRouteData.ts new file mode 100644 index 0000000000000..06394137452e0 --- /dev/null +++ b/packages/core-typings/src/RoomRouteData.ts @@ -0,0 +1,26 @@ +import type { IRoom } from './IRoom'; + +export type DirectRoomRouteData = ( + | { + rid: IRoom['_id']; + name?: IRoom['name']; + } + | { + name: IRoom['name']; + } +) & { + tab?: string; +}; + +export type ChannelRouteData = { + name: IRoom['name']; + tab?: string; + context?: string; +}; + +export type OmnichannelRoomRouteData = { + rid: IRoom['_id']; + tab?: string; +}; + +export type RoomRouteData = DirectRoomRouteData | ChannelRouteData | OmnichannelRoomRouteData; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index c310ba4865165..01bc90d2a6bc8 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -137,4 +137,6 @@ export * from './ICustomOAuthConfig'; export * from './IModerationReport'; export * from './CustomFieldMetadata'; +export * from './RoomRouteData'; + export * as Cloud from './cloud'; diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index a3b2ca725f473..26c6a82c55339 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -69,6 +69,7 @@ export class MockedAppRootBuilder { navigate: () => undefined, subscribeToRouteChange: () => () => undefined, subscribeToRoutesChange: () => () => undefined, + getRoomRoute: () => ({ path: '/' }), }; private settings: Mutable> = { diff --git a/packages/ui-contexts/src/RouterContext.ts b/packages/ui-contexts/src/RouterContext.ts index 3fbd4bd98cf21..5ed0bbec823a9 100644 --- a/packages/ui-contexts/src/RouterContext.ts +++ b/packages/ui-contexts/src/RouterContext.ts @@ -1,3 +1,4 @@ +import type { RoomType, RoomRouteData, DirectRoomRouteData, OmnichannelRoomRouteData, ChannelRouteData } from '@rocket.chat/core-typings'; import type { ReactNode } from 'react'; import { createContext } from 'react'; @@ -65,6 +66,15 @@ export type RouterContextValue = { defineRoutes(routes: RouteObject[]): () => void; getRoutes(): RouteObject[]; subscribeToRoutesChange(onRoutesChange: () => void): () => void; + getRoomRoute(roomType: 'd', routeData: DirectRoomRouteData): { path: LocationPathname }; + getRoomRoute(roomType: 'l' | 'v', routeData: OmnichannelRoomRouteData): { path: LocationPathname }; + getRoomRoute(roomType: 'p' | 'c', routeData: ChannelRouteData): { path: LocationPathname }; + getRoomRoute( + roomType: RoomType, + routeData: RoomRouteData, + ): { + path: LocationPathname; + }; }; export const RouterContext = createContext({ @@ -92,5 +102,8 @@ export const RouterContext = createContext({ getRoutes: () => { throw new Error('not implemented'); }, + getRoomRoute: () => { + throw new Error('not implemented'); + }, subscribeToRoutesChange: () => () => undefined, }); diff --git a/packages/ui-contexts/src/hooks/useGoToRoom.ts b/packages/ui-contexts/src/hooks/useGoToRoom.ts new file mode 100644 index 0000000000000..e809a71e25bbe --- /dev/null +++ b/packages/ui-contexts/src/hooks/useGoToRoom.ts @@ -0,0 +1,29 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; + +import { useEndpoint } from './useEndpoint'; +import { useRouter } from './useRouter'; + +export const useGoToRoom = ({ replace = false }: { replace?: boolean } = {}): ((rid: IRoom['_id']) => void) => { + const router = useRouter(); + const getRoomById = useEndpoint('GET', '/v1/rooms.info'); + + return useEffectEvent(async (roomId) => { + const { room } = await getRoomById({ roomId }); + + if (!room) { + return; + } + + const { t, name, _id: rid } = room; + + const { path } = router.getRoomRoute(t, ['c', 'p'].includes(t) ? { name } : { rid }); + + router.navigate( + { + pathname: path, + }, + { replace }, + ); + }); +}; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 0870eb4417c8a..e433aadaf1a69 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -30,6 +30,7 @@ export { useCurrentModal } from './hooks/useCurrentModal'; export { useCurrentRoutePath } from './hooks/useCurrentRoutePath'; export { useCustomSound } from './hooks/useCustomSound'; export { useEndpoint } from './hooks/useEndpoint'; +export { useGoToRoom } from './hooks/useGoToRoom'; export type { EndpointFunction } from './hooks/useEndpoint'; export { useIsPrivilegedSettingsContext } from './hooks/useIsPrivilegedSettingsContext'; export { useIsSettingsContextLoading } from './hooks/useIsSettingsContextLoading'; From 439faa87d302055d2e1a6b29e9fabdd13a1e653a Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Fri, 19 Jul 2024 19:41:26 -0300 Subject: [PATCH 069/114] feat: Improve Engagement Dashboard's "Channels" tab performance (#32493) --- .changeset/many-tables-love.md | 6 + .changeset/proud-waves-bathe.md | 6 + .../channels/useChannelsList.ts | 1 + .../api/engagementDashboard/channels.ts | 22 +- .../lib/engagementDashboard/channels.ts | 76 +++- apps/meteor/server/models/raw/Analytics.ts | 123 ++++++- apps/meteor/server/models/raw/Rooms.ts | 51 ++- .../end-to-end/api/34-engagement-dashboard.ts | 347 ++++++++++++++++++ .../src/models/IAnalyticsModel.ts | 9 + .../model-typings/src/models/IRoomsModel.ts | 11 +- 10 files changed, 596 insertions(+), 56 deletions(-) create mode 100644 .changeset/many-tables-love.md create mode 100644 .changeset/proud-waves-bathe.md create mode 100644 apps/meteor/tests/end-to-end/api/34-engagement-dashboard.ts diff --git a/.changeset/many-tables-love.md b/.changeset/many-tables-love.md new file mode 100644 index 0000000000000..8f37283c6a967 --- /dev/null +++ b/.changeset/many-tables-love.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +--- + +Fixed Livechat rooms being displayed in the Engagement Dashboard's "Channels" tab diff --git a/.changeset/proud-waves-bathe.md b/.changeset/proud-waves-bathe.md new file mode 100644 index 0000000000000..556fa3af80e12 --- /dev/null +++ b/.changeset/proud-waves-bathe.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +--- + +Improved Engagement Dashboard's "Channels" tab performance by not returning rooms that had no activity in the analyzed period diff --git a/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts b/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts index cd1a338eeab59..f45c620c8b498 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/channels/useChannelsList.ts @@ -24,6 +24,7 @@ export const useChannelsList = ({ period, offset, count }: UseChannelsListOption end: end.toISOString(), offset, count, + hideRoomsWithNoActivity: true, }); return response diff --git a/apps/meteor/ee/server/api/engagementDashboard/channels.ts b/apps/meteor/ee/server/api/engagementDashboard/channels.ts index b2a655f4a8436..0d2d140bd5750 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/channels.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/channels.ts @@ -3,14 +3,15 @@ import { check, Match } from 'meteor/check'; import { API } from '../../../../app/api/server'; import { getPaginationItems } from '../../../../app/api/server/helpers/getPaginationItems'; -import { findAllChannelsWithNumberOfMessages } from '../../lib/engagementDashboard/channels'; +import { apiDeprecationLogger } from '../../../../app/lib/server/lib/deprecationWarningLogger'; +import { findChannelsWithNumberOfMessages } from '../../lib/engagementDashboard/channels'; import { isDateISOString, mapDateForAPI } from '../../lib/engagementDashboard/date'; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention interface Endpoints { '/v1/engagement-dashboard/channels/list': { - GET: (params: { start: string; end: string; offset?: number; count?: number }) => { + GET: (params: { start: string; end: string; offset?: number; count?: number; hideRoomsWithNoActivity?: boolean }) => { channels: { room: { _id: IRoom['_id']; @@ -45,17 +46,30 @@ API.v1.addRoute( Match.ObjectIncluding({ start: Match.Where(isDateISOString), end: Match.Where(isDateISOString), + hideRoomsWithNoActivity: Match.Maybe(String), offset: Match.Maybe(String), count: Match.Maybe(String), }), ); - const { start, end } = this.queryParams; + const { start, end, hideRoomsWithNoActivity } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); - const { channels, total } = await findAllChannelsWithNumberOfMessages({ + if (hideRoomsWithNoActivity === undefined) { + apiDeprecationLogger.deprecatedParameterUsage( + this.request.route, + 'hideRoomsWithNoActivity', + '7.0.0', + this.response, + ({ parameter, endpoint, version }) => + `Returning rooms that had no activity in ${endpoint} is deprecated and will be removed on version ${version} along with the \`${parameter}\` param. Set \`${parameter}\` as \`true\` to check how the endpoint will behave starting on ${version}`, + ); + } + + const { channels, total } = await findChannelsWithNumberOfMessages({ start: mapDateForAPI(start), end: mapDateForAPI(end), + hideRoomsWithNoActivity: hideRoomsWithNoActivity === 'true', options: { offset, count }, }); diff --git a/apps/meteor/ee/server/lib/engagementDashboard/channels.ts b/apps/meteor/ee/server/lib/engagementDashboard/channels.ts index 834284ebb9b7a..7d08086ee1e9b 100644 --- a/apps/meteor/ee/server/lib/engagementDashboard/channels.ts +++ b/apps/meteor/ee/server/lib/engagementDashboard/channels.ts @@ -1,9 +1,69 @@ import type { IDirectMessageRoom, IRoom } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Analytics, Rooms } from '@rocket.chat/models'; import moment from 'moment'; +import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { convertDateToInt, diffBetweenDaysInclusive } from './date'; +export const findChannelsWithNumberOfMessages = async ({ + start, + end, + hideRoomsWithNoActivity, + options = {}, +}: { + start: Date; + end: Date; + hideRoomsWithNoActivity: boolean; + options: { + offset?: number; + count?: number; + }; +}): Promise<{ + channels: { + room: { + _id: IRoom['_id']; + name: IRoom['name'] | IRoom['fname']; + ts: IRoom['ts']; + t: IRoom['t']; + _updatedAt: IRoom['_updatedAt']; + usernames?: IDirectMessageRoom['usernames']; + }; + messages: number; + lastWeekMessages: number; + diffFromLastWeek: number; + }[]; + total: number; +}> => { + if (!hideRoomsWithNoActivity) { + return findAllChannelsWithNumberOfMessages({ start, end, options }); + } + + const daysBetweenDates = diffBetweenDaysInclusive(end, start); + const endOfLastWeek = moment(start).subtract(1, 'days').toDate(); + const startOfLastWeek = moment(endOfLastWeek).subtract(daysBetweenDates, 'days').toDate(); + const roomTypes = roomCoordinator.getTypesToShowOnDashboard() as Array; + + const aggregationResult = await Analytics.findRoomsByTypesWithNumberOfMessagesBetweenDate({ + types: roomTypes, + start: convertDateToInt(start), + end: convertDateToInt(end), + startOfLastWeek: convertDateToInt(startOfLastWeek), + endOfLastWeek: convertDateToInt(endOfLastWeek), + options, + }).toArray(); + + // The aggregation result may be undefined if there are no matching analytics or corresponding rooms in the period + if (!aggregationResult.length) { + return { channels: [], total: 0 }; + } + + const [{ channels, total }] = aggregationResult; + return { + channels, + total, + }; +}; + export const findAllChannelsWithNumberOfMessages = async ({ start, end, @@ -34,8 +94,10 @@ export const findAllChannelsWithNumberOfMessages = async ({ const daysBetweenDates = diffBetweenDaysInclusive(end, start); const endOfLastWeek = moment(start).subtract(1, 'days').toDate(); const startOfLastWeek = moment(endOfLastWeek).subtract(daysBetweenDates, 'days').toDate(); + const roomTypes = roomCoordinator.getTypesToShowOnDashboard() as Array; - const channels = await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ + const channels = await Rooms.findChannelsByTypesWithNumberOfMessagesBetweenDate({ + types: roomTypes, start: convertDateToInt(start), end: convertDateToInt(end), startOfLastWeek: convertDateToInt(startOfLastWeek), @@ -43,15 +105,7 @@ export const findAllChannelsWithNumberOfMessages = async ({ options, }).toArray(); - const total = - ( - await Rooms.countChannelsWithNumberOfMessagesBetweenDate({ - start: convertDateToInt(start), - end: convertDateToInt(end), - startOfLastWeek: convertDateToInt(startOfLastWeek), - endOfLastWeek: convertDateToInt(endOfLastWeek), - }).toArray() - )[0]?.total ?? 0; + const total = await Rooms.countDocuments({ t: { $in: roomTypes } }); return { channels, diff --git a/apps/meteor/server/models/raw/Analytics.ts b/apps/meteor/server/models/raw/Analytics.ts index 4e95cadebbcde..de47008341684 100644 --- a/apps/meteor/server/models/raw/Analytics.ts +++ b/apps/meteor/server/models/raw/Analytics.ts @@ -1,7 +1,7 @@ import type { IAnalytic, IRoom } from '@rocket.chat/core-typings'; -import type { IAnalyticsModel } from '@rocket.chat/model-typings'; +import type { IAnalyticsModel, IChannelsWithNumberOfMessagesBetweenDate } from '@rocket.chat/model-typings'; import { Random } from '@rocket.chat/random'; -import type { AggregationCursor, FindCursor, Db, IndexDescription, FindOptions, UpdateResult, Document } from 'mongodb'; +import type { AggregationCursor, FindCursor, Db, IndexDescription, FindOptions, UpdateResult, Document, Collection } from 'mongodb'; import { readSecondaryPreferred } from '../../database/readSecondaryPreferred'; import { BaseRaw } from './BaseRaw'; @@ -14,7 +14,11 @@ export class AnalyticsRaw extends BaseRaw implements IAnalyticsModel } protected modelIndexes(): IndexDescription[] { - return [{ key: { date: 1 } }, { key: { 'room._id': 1, 'date': 1 }, unique: true, partialFilterExpression: { type: 'rooms' } }]; + return [ + { key: { date: 1 } }, + { key: { 'room._id': 1, 'date': 1 }, unique: true, partialFilterExpression: { type: 'rooms' } }, + { key: { 'room.t': 1, 'date': 1 }, partialFilterExpression: { type: 'messages' } }, + ]; } saveMessageSent({ room, date }: { room: IRoom; date: IAnalytic['date'] }): Promise { @@ -211,4 +215,117 @@ export class AnalyticsRaw extends BaseRaw implements IAnalyticsModel findByTypeBeforeDate({ type, date }: { type: IAnalytic['type']; date: IAnalytic['date'] }): FindCursor { return this.find({ type, date: { $lte: date } }); } + + getRoomsWithNumberOfMessagesBetweenDateQuery({ + types, + start, + end, + startOfLastWeek, + endOfLastWeek, + options, + }: { + types: Array; + start: number; + end: number; + startOfLastWeek: number; + endOfLastWeek: number; + options?: any; + }) { + const typeAndDateMatch = { + $match: { + 'type': 'messages', + 'room.t': { $in: types }, + 'date': { $gte: startOfLastWeek, $lte: end }, + }, + }; + const roomsGroup = { + $group: { + _id: '$room._id', + room: { $first: '$room' }, + messages: { $sum: { $cond: [{ $gte: ['$date', start] }, '$messages', 0] } }, + lastWeekMessages: { $sum: { $cond: [{ $lte: ['$date', endOfLastWeek] }, '$messages', 0] } }, + }, + }; + const lookup = { + $lookup: { + from: 'rocketchat_room', + localField: '_id', + foreignField: '_id', + as: 'room', + }, + }; + const roomsUnwind = { + $unwind: { + path: '$room', + preserveNullAndEmptyArrays: false, + }, + }; + const project = { + $project: { + _id: 0, + room: { + _id: '$room._id', + name: { $ifNull: ['$room.name', '$room.fname'] }, + ts: '$room.ts', + t: '$room.t', + _updatedAt: '$room._updatedAt', + usernames: '$room.usernames', + }, + messages: '$messages', + lastWeekMessages: '$lastWeekMessages', + diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] }, + }, + }; + + const sort = { $sort: options?.sort || { messages: -1 } }; + const sortAndPaginationParams: Exclude['aggregate']>[0], undefined> = [sort]; + if (options?.offset) { + sortAndPaginationParams.push({ $skip: options.offset }); + } + + if (options?.count) { + sortAndPaginationParams.push({ $limit: options.count }); + } + const facet = { + $facet: { + channels: [...sortAndPaginationParams], + total: [{ $count: 'total' }], + }, + }; + const totalUnwind = { $unwind: '$total' }; + const totalProject = { + $project: { + channels: '$channels', + total: '$total.total', + }, + }; + + const params: Exclude['aggregate']>[0], undefined> = [ + typeAndDateMatch, + roomsGroup, + lookup, + roomsUnwind, + project, + facet, + totalUnwind, + totalProject, + ]; + + return params; + } + + findRoomsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; + start: number; + end: number; + startOfLastWeek: number; + endOfLastWeek: number; + options?: any; + }): AggregationCursor<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }> { + const aggregationParams = this.getRoomsWithNumberOfMessagesBetweenDateQuery(params); + return this.col.aggregate<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }>(aggregationParams, { + allowDiskUse: true, + readPreference: readSecondaryPreferred(), + }); + } } diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 1859f6c18b356..a4cd19a1c30a8 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -413,18 +413,25 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { } getChannelsWithNumberOfMessagesBetweenDateQuery({ + types, start, end, startOfLastWeek, endOfLastWeek, options, }: { + types: Array; start: number; end: number; startOfLastWeek: number; endOfLastWeek: number; options?: any; }) { + const typeMatch = { + $match: { + t: { $in: types }, + }, + }; const lookup = { $lookup: { from: 'rocketchat_analytics', @@ -504,30 +511,32 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] }, }, }; - const firstParams = [ - lookup, - messagesProject, - messagesUnwind, - messagesGroup, - lastWeekMessagesUnwind, - lastWeekMessagesGroup, - presentationProject, - ]; + const firstParams = [typeMatch, lookup, messagesProject, messagesUnwind, messagesGroup]; + const lastParams = [lastWeekMessagesUnwind, lastWeekMessagesGroup, presentationProject]; + const sort = { $sort: options?.sort || { messages: -1 } }; - const params: Exclude['aggregate']>[0], undefined> = [...firstParams, sort]; + const sortAndPaginationParams: Exclude['aggregate']>[0], undefined> = [sort]; if (options?.offset) { - params.push({ $skip: options.offset }); + sortAndPaginationParams.push({ $skip: options.offset }); } if (options?.count) { - params.push({ $limit: options.count }); + sortAndPaginationParams.push({ $limit: options.count }); + } + const params: Exclude['aggregate']>[0], undefined> = [...firstParams]; + + if (options?.sort) { + params.push(...lastParams, ...sortAndPaginationParams); + } else { + params.push(...sortAndPaginationParams, ...lastParams, sort); } return params; } - findChannelsWithNumberOfMessagesBetweenDate(params: { + findChannelsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; start: number; end: number; startOfLastWeek: number; @@ -541,22 +550,6 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { }); } - countChannelsWithNumberOfMessagesBetweenDate(params: { - start: number; - end: number; - startOfLastWeek: number; - endOfLastWeek: number; - options?: any; - }): AggregationCursor<{ total: number }> { - const aggregationParams = this.getChannelsWithNumberOfMessagesBetweenDateQuery(params); - aggregationParams.push({ $count: 'total' }); - - return this.col.aggregate<{ total: number }>(aggregationParams, { - allowDiskUse: true, - readPreference: readSecondaryPreferred(), - }); - } - findOneByNameOrFname(name: NonNullable, options: FindOptions = {}): Promise { const query = { $or: [ diff --git a/apps/meteor/tests/end-to-end/api/34-engagement-dashboard.ts b/apps/meteor/tests/end-to-end/api/34-engagement-dashboard.ts new file mode 100644 index 0000000000000..c1fc685d11f93 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/34-engagement-dashboard.ts @@ -0,0 +1,347 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { after, before, describe, it } from 'mocha'; +import type { Response } from 'supertest'; + +import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { sendSimpleMessage } from '../../data/chat.helper'; +import { updatePermission } from '../../data/permissions.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; + +describe('[Engagement Dashboard]', function () { + this.retries(0); + + const isEnterprise = Boolean(process.env.IS_EE); + + before((done) => getCredentials(done)); + + before(() => updatePermission('view-engagement-dashboard', ['admin'])); + + after(() => updatePermission('view-engagement-dashboard', ['admin'])); + + (isEnterprise ? describe : describe.skip)('[/engagement-dashboard/channels/list]', () => { + let testRoom: IRoom; + + before(async () => { + testRoom = (await createRoom({ type: 'c', name: `channel.test.engagement.${Date.now()}-${Math.random()}` })).body.channel; + }); + + after(async () => { + await deleteRoom({ type: 'c', roomId: testRoom._id }); + }); + + it('should fail if user does not have the view-engagement-dashboard permission', async () => { + await updatePermission('view-engagement-dashboard', []); + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + offset: 0, + count: 25, + }) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + }); + }); + + it('should fail if start param is not a valid date', async () => { + await updatePermission('view-engagement-dashboard', ['admin']); + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + start: 'invalid-date', + end: new Date().toISOString(), + offset: 0, + count: 25, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('Match error: Failed Match.Where validation in field start'); + }); + }); + + it('should fail if end param is not a valid date', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + end: 'invalid-date', + offset: 0, + count: 25, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('Match error: Failed Match.Where validation in field end'); + }); + }); + + it('should fail if start param is not provided', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date(), + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal("Match error: Missing key 'start'"); + }); + }); + + it('should fail if end param is not provided', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal("Match error: Missing key 'end'"); + }); + }); + + it('should succesfuly return results', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + expect(res.body.channels[0]).to.be.an('object').that.is.not.empty; + expect(res.body.channels[0]).to.have.property('messages').that.is.a('number'); + expect(res.body.channels[0]).to.have.property('lastWeekMessages').that.is.a('number'); + expect(res.body.channels[0]).to.have.property('diffFromLastWeek').that.is.a('number'); + expect(res.body.channels[0].room).to.be.an('object').that.is.not.empty; + + expect(res.body.channels[0].room).to.have.property('_id').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('name').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('ts').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('t').that.is.a('string'); + expect(res.body.channels[0].room).to.have.property('_updatedAt').that.is.a('string'); + }); + }); + + it('should not return empty rooms when the hideRoomsWithNoActivity param is provided', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + hideRoomsWithNoActivity: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).to.be.undefined; + }); + }); + + it('should correctly count messages in an empty room', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 0); + expect(channelRecord).to.have.property('lastWeekMessages', 0); + expect(channelRecord).to.have.property('diffFromLastWeek', 0); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + expect(channelRecord.room).to.have.property('_updatedAt', testRoom._updatedAt); + }); + }); + + it('should correctly count messages diff compared to last week when the hideRoomsWithNoActivity param is provided and there are messages in a room', async () => { + await sendSimpleMessage({ roomId: testRoom._id }); + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + hideRoomsWithNoActivity: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 1); + expect(channelRecord).to.have.property('lastWeekMessages', 0); + expect(channelRecord).to.have.property('diffFromLastWeek', 1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + + it('should correctly count messages diff compared to last week when there are messages in a room', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date().toISOString(), + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 1); + expect(channelRecord).to.have.property('lastWeekMessages', 0); + expect(channelRecord).to.have.property('diffFromLastWeek', 1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + + it('should correctly count messages from last week and diff when moving to the next week and providing the hideRoomsWithNoActivity param', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(), + start: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + hideRoomsWithNoActivity: true, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 0); + expect(channelRecord).to.have.property('lastWeekMessages', 1); + expect(channelRecord).to.have.property('diffFromLastWeek', -1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + + it('should correctly count messages from last week and diff when moving to the next week', async () => { + await request + .get(api('engagement-dashboard/channels/list')) + .set(credentials) + .query({ + end: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000).toISOString(), + start: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset', 0); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('channels'); + expect(res.body.channels).to.be.an('array').that.is.not.empty; + + const channelRecord = res.body.channels.find(({ room }: { room: { _id: string } }) => room._id === testRoom._id); + expect(channelRecord).not.to.be.undefined; + + expect(channelRecord).to.be.an('object').that.is.not.empty; + expect(channelRecord).to.have.property('messages', 0); + expect(channelRecord).to.have.property('lastWeekMessages', 1); + expect(channelRecord).to.have.property('diffFromLastWeek', -1); + expect(channelRecord.room).to.be.an('object').that.is.not.empty; + + expect(channelRecord.room).to.have.property('_id', testRoom._id); + expect(channelRecord.room).to.have.property('name', testRoom.name); + expect(channelRecord.room).to.have.property('ts', testRoom.ts); + expect(channelRecord.room).to.have.property('t', testRoom.t); + }); + }); + }); +}); diff --git a/packages/model-typings/src/models/IAnalyticsModel.ts b/packages/model-typings/src/models/IAnalyticsModel.ts index 4d31f66a94d84..1ca4d7abf1186 100644 --- a/packages/model-typings/src/models/IAnalyticsModel.ts +++ b/packages/model-typings/src/models/IAnalyticsModel.ts @@ -2,6 +2,7 @@ import type { IAnalytic, IRoom } from '@rocket.chat/core-typings'; import type { AggregationCursor, FindCursor, FindOptions, UpdateResult, Document } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +import type { IChannelsWithNumberOfMessagesBetweenDate } from './IRoomsModel'; export interface IAnalyticsModel extends IBaseModel { saveMessageSent({ room, date }: { room: IRoom; date: IAnalytic['date'] }): Promise; @@ -38,4 +39,12 @@ export interface IAnalyticsModel extends IBaseModel { users: number; }>; findByTypeBeforeDate({ type, date }: { type: IAnalytic['type']; date: IAnalytic['date'] }): FindCursor; + findRoomsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; + start: number; + end: number; + startOfLastWeek: number; + endOfLastWeek: number; + options?: any; + }): AggregationCursor<{ channels: IChannelsWithNumberOfMessagesBetweenDate[]; total: number }>; } diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 887f51987ae68..f9daef91dece9 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -86,7 +86,8 @@ export interface IRoomsModel extends IBaseModel { setTeamDefaultById(rid: IRoom['_id'], teamDefault: NonNullable, options?: UpdateOptions): Promise; - findChannelsWithNumberOfMessagesBetweenDate(params: { + findChannelsByTypesWithNumberOfMessagesBetweenDate(params: { + types: Array; start: number; end: number; startOfLastWeek: number; @@ -94,14 +95,6 @@ export interface IRoomsModel extends IBaseModel { options?: any; }): AggregationCursor; - countChannelsWithNumberOfMessagesBetweenDate(params: { - start: number; - end: number; - startOfLastWeek: number; - endOfLastWeek: number; - options?: any; - }): AggregationCursor<{ total: number }>; - findOneByName(name: NonNullable, options?: FindOptions): Promise; findDefaultRoomsForTeam(teamId: any): FindCursor; From 7b9b950c67b7db17f191e1fdb242c238ab9af0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Fri, 19 Jul 2024 20:39:26 -0300 Subject: [PATCH 070/114] regression: New Navigation tweaks (#32813) --- .../UserMenu/UserMenu.tsx | 6 +++-- .../client/sidebarv2/header/SearchSection.tsx | 2 +- .../meteor/client/views/room/RoomSkeleton.tsx | 13 +++++++++- .../client/views/room/layout/RoomLayout.tsx | 17 +++++++++++- apps/meteor/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/uikit-playground/package.json | 2 +- yarn.lock | 26 +++++++++---------- 14 files changed, 55 insertions(+), 27 deletions(-) diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx index fd4498f5fb8e3..531ff8a74b663 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx @@ -1,5 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps } from 'react'; import React, { memo, useState } from 'react'; import GenericMenu from '../../../components/GenericMenu/GenericMenu'; @@ -8,9 +9,9 @@ import { useHandleMenuAction } from '../../../components/GenericMenu/hooks/useHa import UserMenuButton from './UserMenuButton'; import { useUserMenu } from './hooks/useUserMenu'; -type UserMenuProps = { user: IUser; className?: string }; +type UserMenuProps = { user: IUser } & Omit, 'sections' | 'items' | 'title'>; -const UserMenu = function UserMenu({ user }: UserMenuProps) { +const UserMenu = function UserMenu({ user, ...props }: UserMenuProps) { const t = useTranslation(); const [isOpen, setIsOpen] = useState(false); @@ -21,6 +22,7 @@ const UserMenu = function UserMenu({ user }: UserMenuProps) { return ( { ( } + header={ + + + + + + + + + } body={ <> diff --git a/apps/meteor/client/views/room/layout/RoomLayout.tsx b/apps/meteor/client/views/room/layout/RoomLayout.tsx index e7e8b1c4f0d12..fd080b916b705 100644 --- a/apps/meteor/client/views/room/layout/RoomLayout.tsx +++ b/apps/meteor/client/views/room/layout/RoomLayout.tsx @@ -1,9 +1,11 @@ import { Box } from '@rocket.chat/fuselage'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import type { ComponentProps, ReactElement, ReactNode } from 'react'; import React, { Suspense } from 'react'; import { ContextualbarDialog } from '../../../components/Contextualbar'; import HeaderSkeleton from '../Header/HeaderSkeleton'; +import HeaderSkeletonV2 from '../HeaderV2/HeaderSkeleton'; type RoomLayoutProps = { header?: ReactNode; @@ -14,7 +16,20 @@ type RoomLayoutProps = { const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): ReactElement => ( - }>{header} + + + + + + + + + } + > + {header} + diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 0a4877dbeede6..8c7c641c4bc27 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -242,7 +242,7 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.2", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.3", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.32.0", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index add24d33a8abb..601d6307750ac 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 622741719481d..57f59f2bcecaf 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -66,7 +66,7 @@ "@rocket.chat/apps-engine": "alpha", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "^0.36.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 03cdcd09d29fb..5c36a430d2336 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/fuselage-tokens": "^0.33.1", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/styled": "~0.31.25", diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 2e45c20f1d36a..69f06db62d82d 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@babel/core": "~7.22.20", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/ui-contexts": "workspace:^", "@types/babel__core": "~7.20.3", "@types/react": "~17.0.69", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index fe35430364cf2..ca94d3e2fac82 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/mock-providers": "workspace:^", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index fdc00ad086e16..0f6ee4655877c 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/icons": "^0.36.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index efacce7a45869..5d5a04916f2c5 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "^0.36.0", "@rocket.chat/styled": "~0.31.25", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index a9293ff218cec..310d391813798 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -15,7 +15,7 @@ "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.55.2", + "@rocket.chat/fuselage": "^0.56.0", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.32.0", diff --git a/yarn.lock b/yarn.lock index 6d962b0844935..85d64c9021a7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8916,7 +8916,7 @@ __metadata: "@rocket.chat/apps-engine": alpha "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/gazzodown": "workspace:^" @@ -8978,9 +8978,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.55.2": - version: 0.55.2 - resolution: "@rocket.chat/fuselage@npm:0.55.2" +"@rocket.chat/fuselage@npm:^0.56.0": + version: 0.56.0 + resolution: "@rocket.chat/fuselage@npm:0.56.0" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 @@ -8998,7 +8998,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 286f4ac261621a09de74e34ef52f5c473e7c2e55ca977507ab1b1fdccf7274c2ca788a42d1415ffd4f4f629377b0bb1ed9ad70ddead7b46d3b422bac1b861431 + checksum: 1817eb660c9581906a645b8d39cd0377395085ab7dc204d7afd3b2b1c1a36000ea3727a8263ff02dc3d51b8280e1b51e42d062587d685f7e0be1a7a6eb97c5bf languageName: node linkType: hard @@ -9009,7 +9009,7 @@ __metadata: "@babel/core": ~7.22.20 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/fuselage-tokens": ^0.33.1 "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/styled": ~0.31.25 @@ -9370,7 +9370,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.2 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.3 - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.32.0 @@ -10266,7 +10266,7 @@ __metadata: resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": ~7.22.20 - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/ui-contexts": "workspace:^" "@types/babel__core": ~7.20.3 "@types/react": ~17.0.69 @@ -10292,7 +10292,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ^0.36.0 "@rocket.chat/mock-providers": "workspace:^" @@ -10345,7 +10345,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/icons": ^0.36.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -10437,7 +10437,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ^0.36.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -10480,7 +10480,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ^0.36.0 "@rocket.chat/styled": ~0.31.25 @@ -10525,7 +10525,7 @@ __metadata: "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.55.2 + "@rocket.chat/fuselage": ^0.56.0 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.32.0 From e9fae8e4db36fe5ba9a2104b76459072ff7fd947 Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Sat, 20 Jul 2024 03:52:18 +0200 Subject: [PATCH 071/114] fix: remove hide option on omnichannel conversations (#32691) Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- .changeset/popular-trees-lay.md | 5 +++++ apps/meteor/client/sidebar/RoomMenu.tsx | 12 ++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .changeset/popular-trees-lay.md diff --git a/.changeset/popular-trees-lay.md b/.changeset/popular-trees-lay.md new file mode 100644 index 0000000000000..f38ef1f923670 --- /dev/null +++ b/.changeset/popular-trees-lay.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Removed 'Hide' option in the room menu for Omnichannel conversations. diff --git a/apps/meteor/client/sidebar/RoomMenu.tsx b/apps/meteor/client/sidebar/RoomMenu.tsx index 8df55bd5d3594..06b1352d28031 100644 --- a/apps/meteor/client/sidebar/RoomMenu.tsx +++ b/apps/meteor/client/sidebar/RoomMenu.tsx @@ -200,10 +200,14 @@ const RoomMenu = ({ const menuOptions = useMemo( () => ({ ...(!hideDefaultOptions && { - hideRoom: { - label: { label: t('Hide'), icon: 'eye-off' }, - action: handleHide, - }, + ...(isOmnichannelRoom + ? {} + : { + hideRoom: { + label: { label: t('Hide'), icon: 'eye-off' }, + action: handleHide, + }, + }), toggleRead: { label: { label: isUnread ? t('Mark_read') : t('Mark_unread'), icon: 'flag' }, action: handleToggleRead, From d4d144530c62293c785e5b10ce84c39640bfb8d8 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Sat, 20 Jul 2024 08:25:35 +0530 Subject: [PATCH 072/114] fix: update cached setting immediately at the time of updating the db (#32742) --- .changeset/mean-hairs-move.md | 5 ++ .../app/settings/server/SettingsRegistry.ts | 4 +- .../server/functions/settings.mocks.ts | 41 +++++++-- .../compareSettingsMetadata.tests.ts | 28 +++++++ .../server/functions/settings.tests.ts | 84 +++++++++++++++++++ 5 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 .changeset/mean-hairs-move.md create mode 100644 apps/meteor/tests/unit/app/settings/server/functions/compareSettingsMetadata.tests.ts diff --git a/.changeset/mean-hairs-move.md b/.changeset/mean-hairs-move.md new file mode 100644 index 0000000000000..c92293d6ae953 --- /dev/null +++ b/.changeset/mean-hairs-move.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Fixed an issue where adding `OVERWRITE_SETTING_` for any setting wasn't immediately taking effect sometimes, and needed a server restart to reflect. diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts index 5783e2946dc1f..e86a391ad8fa1 100644 --- a/apps/meteor/app/settings/server/SettingsRegistry.ts +++ b/apps/meteor/app/settings/server/SettingsRegistry.ts @@ -73,7 +73,7 @@ const compareSettingsIgnoringKeys = .filter((key) => !keys.includes(key as keyof ISetting)) .every((key) => isEqual(a[key as keyof ISetting], b[key as keyof ISetting])); -const compareSettings = compareSettingsIgnoringKeys([ +export const compareSettings = compareSettingsIgnoringKeys([ 'value', 'ts', 'createdAt', @@ -166,6 +166,7 @@ export class SettingsRegistry { })(); await this.saveUpdatedSetting(_id, updatedProps, removedKeys); + this.store.set(settingFromCodeOverwritten); return; } @@ -175,6 +176,7 @@ export class SettingsRegistry { const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key)); await this.saveUpdatedSetting(_id, settingProps, removedKeys); + this.store.set(settingFromCodeOverwritten); } return; } diff --git a/apps/meteor/app/settings/server/functions/settings.mocks.ts b/apps/meteor/app/settings/server/functions/settings.mocks.ts index 9cd409ba0b837..fb31c3021b1b3 100644 --- a/apps/meteor/app/settings/server/functions/settings.mocks.ts +++ b/apps/meteor/app/settings/server/functions/settings.mocks.ts @@ -9,6 +9,12 @@ type Dictionary = { class SettingsClass { settings: ICachedSettings; + private delay = 0; + + setDelay(delay: number): void { + this.delay = delay; + } + find(): any[] { return []; } @@ -65,22 +71,41 @@ class SettingsClass { throw new Error('Invalid upsert'); } - // console.log(query, data); - this.data.set(query._id, data); - - // Can't import before the mock command on end of this file! - // eslint-disable-next-line @typescript-eslint/no-var-requires - this.settings.set(data); + if (this.delay) { + setTimeout(() => { + // console.log(query, data); + this.data.set(query._id, data); + + // Can't import before the mock command on end of this file! + // eslint-disable-next-line @typescript-eslint/no-var-requires + this.settings.set(data); + }, this.delay); + } else { + this.data.set(query._id, data); + // Can't import before the mock command on end of this file! + // eslint-disable-next-line @typescript-eslint/no-var-requires + this.settings.set(data); + } this.upsertCalls++; } + findOneAndUpdate({ _id }: { _id: string }, value: any, options?: any) { + this.updateOne({ _id }, value, options); + return { value: this.findOne({ _id }) }; + } + updateValueById(id: string, value: any): void { this.data.set(id, { ...this.data.get(id), value }); - // Can't import before the mock command on end of this file! // eslint-disable-next-line @typescript-eslint/no-var-requires - this.settings.set(this.data.get(id) as ISetting); + if (this.delay) { + setTimeout(() => { + this.settings.set(this.data.get(id) as ISetting); + }, this.delay); + } else { + this.settings.set(this.data.get(id) as ISetting); + } } } diff --git a/apps/meteor/tests/unit/app/settings/server/functions/compareSettingsMetadata.tests.ts b/apps/meteor/tests/unit/app/settings/server/functions/compareSettingsMetadata.tests.ts new file mode 100644 index 0000000000000..e06249a8e7f8b --- /dev/null +++ b/apps/meteor/tests/unit/app/settings/server/functions/compareSettingsMetadata.tests.ts @@ -0,0 +1,28 @@ +import { expect } from 'chai'; + +import { compareSettings } from '../../../../../../app/settings/server/SettingsRegistry'; +import { getSettingDefaults } from '../../../../../../app/settings/server/functions/getSettingDefaults'; + +const testSetting = getSettingDefaults({ + _id: 'my_dummy_setting', + type: 'string', + value: 'dummy', +}); + +describe('#compareSettings', () => { + const ignoredKeys = ['value', 'ts', 'createdAt', 'valueSource', 'packageValue', 'processEnvValue', '_updatedAt']; + + ignoredKeys.forEach((key) => + it(`should return true if ${key} changes`, () => { + const copiedSetting: any = { ...testSetting }; + + if (['ts', 'createdAt', '_updatedAt'].includes(key)) { + copiedSetting[key] = new Date(); + } else { + copiedSetting[key] = 'random'; + } + + expect(compareSettings(testSetting, copiedSetting)).to.be.true; + }), + ); +}); diff --git a/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts b/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts index 272fa950a407b..3f409881b259d 100644 --- a/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts +++ b/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts @@ -3,13 +3,21 @@ import { beforeEach, describe, it } from 'mocha'; import { CachedSettings } from '../../../../../../app/settings/server/CachedSettings'; import { SettingsRegistry } from '../../../../../../app/settings/server/SettingsRegistry'; +import { getSettingDefaults } from '../../../../../../app/settings/server/functions/getSettingDefaults'; import { Settings } from '../../../../../../app/settings/server/functions/settings.mocks'; +const testSetting = getSettingDefaults({ + _id: 'my_dummy_setting', + type: 'string', + value: 'dummy', +}); + describe('Settings', () => { beforeEach(() => { Settings.insertCalls = 0; Settings.upsertCalls = 0; process.env = {}; + Settings.setDelay(0); }); it('should not insert the same setting twice', async () => { @@ -440,6 +448,67 @@ describe('Settings', () => { .to.not.have.any.keys('section'); }); + it('should ignore setting object from code if only value changes and setting already stored', async () => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initialized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + + await settingsRegistry.add(testSetting._id, testSetting.value, testSetting); + + expect(Settings.insertCalls).to.be.equal(1); + Settings.insertCalls = 0; + + const settingFromCodeFaked = { ...testSetting, value: Date.now().toString() }; + + await settingsRegistry.add(settingFromCodeFaked._id, settingFromCodeFaked.value, settingFromCodeFaked); + + expect(Settings.insertCalls).to.be.equal(0); + expect(Settings.upsertCalls).to.be.equal(0); + }); + + it('should ignore value from environment if setting is already stored', async () => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initialized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + + await settingsRegistry.add(testSetting._id, testSetting.value, testSetting); + + process.env[testSetting._id] = Date.now().toString(); + + await settingsRegistry.add(testSetting._id, testSetting.value, testSetting); + + expect(Settings.findOne({ _id: testSetting._id }).value).to.be.equal(testSetting.value); + }); + + it('should update setting cache synchronously if overwrite is available in enviornment', async () => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initialized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + + process.env[`OVERWRITE_SETTING_${testSetting._id}`] = Date.now().toString(); + + await settingsRegistry.add(testSetting._id, testSetting.value, testSetting); + + expect(settings.get(testSetting._id)).to.be.equal(process.env[`OVERWRITE_SETTING_${testSetting._id}`]); + }); + + it('should update cached value with OVERWRITE_SETTING value even if both with-prefixed and without-prefixed variables exist', async () => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initialized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + + process.env[`OVERWRITE_SETTING_${testSetting._id}`] = Date.now().toString(); + process.env[testSetting._id] = Date.now().toString(); + + await settingsRegistry.add(testSetting._id, testSetting.value, testSetting); + + expect(Settings.findOne({ _id: testSetting._id }).value).to.be.equal(process.env[`OVERWRITE_SETTING_${testSetting._id}`]); + }); + it('should call `settings.get` callback on setting added', async () => { return new Promise(async (resolve) => { const settings = new CachedSettings(); @@ -502,4 +571,19 @@ describe('Settings', () => { }, settings.getConfig({ debounce: 10 }).debounce); }); }); + + it('should update the stored value on setting change', async () => { + Settings.setDelay(10); + process.env[`OVERWRITE_SETTING_${testSetting._id}`] = 'false'; + const settings = new CachedSettings(); + Settings.settings = settings; + + settings.set(testSetting); + settings.initialized(); + + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + await settingsRegistry.add(testSetting._id, testSetting.value, testSetting); + + expect(settings.get(testSetting._id)).to.be.equal(false); + }); }); From 35985d7e1856a21ea39b3f9e7a9445f6d19ec00a Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Sat, 20 Jul 2024 09:40:30 -0300 Subject: [PATCH 073/114] fix: Rooms, teams and roles LDAP syncs are not triggered on login (#32445) --- .changeset/quick-ducks-live.md | 5 +++++ apps/meteor/server/lib/ldap/Connection.ts | 12 ++++++------ apps/meteor/server/lib/ldap/Manager.ts | 2 ++ 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/quick-ducks-live.md diff --git a/.changeset/quick-ducks-live.md b/.changeset/quick-ducks-live.md new file mode 100644 index 0000000000000..ad628c13d0874 --- /dev/null +++ b/.changeset/quick-ducks-live.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed LDAP rooms, teams and roles syncs not being triggered on login even when the "Update User Data on Login" setting is enabled diff --git a/apps/meteor/server/lib/ldap/Connection.ts b/apps/meteor/server/lib/ldap/Connection.ts index 167f1b36e5081..b753a2baa9dc6 100644 --- a/apps/meteor/server/lib/ldap/Connection.ts +++ b/apps/meteor/server/lib/ldap/Connection.ts @@ -660,12 +660,8 @@ export class LDAPConnection { this.client._updateIdle(override); } - protected async maybeBindDN(): Promise { - if (this.usingAuthentication) { - return; - } - - if (!this.options.authentication) { + protected async maybeBindDN({ forceBindAuthenticationUser = false } = {}): Promise { + if (!forceBindAuthenticationUser && (this.usingAuthentication || !this.options.authentication)) { return; } @@ -692,6 +688,10 @@ export class LDAPConnection { return this.maybeBindDN(); } + public async bindAuthenticationUser(): Promise { + return this.maybeBindDN({ forceBindAuthenticationUser: true }); + } + /* Get list of options to initialize a new ldapjs Client */ diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index 4a5cdf2df8d6e..ab000b1422256 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -44,6 +44,8 @@ export class LDAPManager { const slugifiedUsername = this.slugifyUsername(ldapUser, username); const user = await this.findExistingUser(ldapUser, slugifiedUsername); + // Bind connection to the admin user so that RC has full access to groups in the next steps + await ldap.bindAuthenticationUser(); if (user) { return await this.loginExistingUser(ldap, user, ldapUser, password); } From 88e5219bd22a16e1f14a919622c68a0fc6125fe3 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Mon, 22 Jul 2024 00:47:15 -0300 Subject: [PATCH 074/114] fix(UiKit): Modal validation not working (#32679) --- .changeset/chatty-hounds-hammer.md | 6 +++ .../uikit/hooks/useModalContextValue.ts | 4 +- .../client/views/modal/uikit/UiKitModal.tsx | 2 +- .../{ => apps}/app-modal-interaction.spec.ts | 8 +-- .../apps-contextualbar.spec.ts} | 20 +++++--- apps/meteor/tests/e2e/apps/apps-modal.spec.ts | 49 +++++++++++++++++++ apps/meteor/tests/e2e/fixtures/insert-apps.ts | 2 +- apps/meteor/tests/e2e/page-objects/modal.ts | 21 ++++++++ .../src/contexts/UiKitContext.ts | 2 +- .../src/hooks/useUiKitState.ts | 6 ++- 10 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 .changeset/chatty-hounds-hammer.md rename apps/meteor/tests/e2e/{ => apps}/app-modal-interaction.spec.ts (92%) rename apps/meteor/tests/e2e/{apps.spec.ts => apps/apps-contextualbar.spec.ts} (60%) create mode 100644 apps/meteor/tests/e2e/apps/apps-modal.spec.ts create mode 100644 apps/meteor/tests/e2e/page-objects/modal.ts diff --git a/.changeset/chatty-hounds-hammer.md b/.changeset/chatty-hounds-hammer.md new file mode 100644 index 0000000000000..1a2d3a7de559c --- /dev/null +++ b/.changeset/chatty-hounds-hammer.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/fuselage-ui-kit": patch +--- + +Fix validations from "UiKit" modal component diff --git a/apps/meteor/client/uikit/hooks/useModalContextValue.ts b/apps/meteor/client/uikit/hooks/useModalContextValue.ts index e10a59902b6b5..4a0932c8e1e73 100644 --- a/apps/meteor/client/uikit/hooks/useModalContextValue.ts +++ b/apps/meteor/client/uikit/hooks/useModalContextValue.ts @@ -14,6 +14,7 @@ type UseModalContextValueParams = { blockId?: string | undefined; }; }; + errors?: { [field: string]: string }[] | { [field: string]: string }; updateValues: Dispatch<{ actionId: string; payload: { @@ -25,7 +26,7 @@ type UseModalContextValueParams = { type UseModalContextValueReturn = ContextType; -export const useModalContextValue = ({ view, values, updateValues }: UseModalContextValueParams): UseModalContextValueReturn => { +export const useModalContextValue = ({ view, errors, values, updateValues }: UseModalContextValueParams): UseModalContextValueReturn => { const actionManager = useUiKitActionManager(); const emitInteraction = useMemo(() => actionManager.emitInteraction.bind(actionManager), [actionManager]); @@ -62,6 +63,7 @@ export const useModalContextValue = ({ view, values, updateValues }: UseModalCon }); }, ...view, + errors, values, viewId: view.id, }; diff --git a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx index 56242d399ac52..55c6c8a32d71f 100644 --- a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx +++ b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx @@ -20,7 +20,7 @@ type UiKitModalProps = { const UiKitModal = ({ initialView }: UiKitModalProps) => { const actionManager = useUiKitActionManager(); const { view, errors, values, updateValues, state } = useUiKitView(initialView); - const contextValue = useModalContextValue({ view, values, updateValues }); + const contextValue = useModalContextValue({ view, errors, values, updateValues }); const handleSubmit = useEffectEvent((e: FormEvent) => { preventSyntheticEvent(e); diff --git a/apps/meteor/tests/e2e/app-modal-interaction.spec.ts b/apps/meteor/tests/e2e/apps/app-modal-interaction.spec.ts similarity index 92% rename from apps/meteor/tests/e2e/app-modal-interaction.spec.ts rename to apps/meteor/tests/e2e/apps/app-modal-interaction.spec.ts index 1a1ce2b656e89..e2f25e913427a 100644 --- a/apps/meteor/tests/e2e/app-modal-interaction.spec.ts +++ b/apps/meteor/tests/e2e/apps/app-modal-interaction.spec.ts @@ -1,7 +1,7 @@ -import { Users } from './fixtures/userStates'; -import { HomeChannel } from './page-objects'; -import { createTargetChannel } from './utils/create-target-channel'; -import { test, expect } from './utils/test'; +import { Users } from '../fixtures/userStates'; +import { HomeChannel } from '../page-objects'; +import { createTargetChannel } from '../utils/create-target-channel'; +import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); diff --git a/apps/meteor/tests/e2e/apps.spec.ts b/apps/meteor/tests/e2e/apps/apps-contextualbar.spec.ts similarity index 60% rename from apps/meteor/tests/e2e/apps.spec.ts rename to apps/meteor/tests/e2e/apps/apps-contextualbar.spec.ts index bc41cf7aed069..4753d5167083f 100644 --- a/apps/meteor/tests/e2e/apps.spec.ts +++ b/apps/meteor/tests/e2e/apps/apps-contextualbar.spec.ts @@ -1,26 +1,34 @@ -import { Users } from './fixtures/userStates'; -import { HomeChannel } from './page-objects'; -import { expect, test } from './utils/test'; +import type { Page } from '@playwright/test'; + +import { Users } from '../fixtures/userStates'; +import { HomeChannel } from '../page-objects'; +import { expect, test } from '../utils/test'; test.use({ storageState: Users.user1.state }); -test.describe.serial('Apps', () => { +test.describe.serial('Apps > ContextualBar', () => { let poHomeChannel: HomeChannel; - test.beforeEach(async ({ page }) => { + let page: Page; + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); poHomeChannel = new HomeChannel(page); await page.goto('/home'); await poHomeChannel.sidenav.openChat('general'); }); + test.afterAll(async () => { + await page.close(); + }); + test('expect allow user open app contextualbar', async () => { await poHomeChannel.content.dispatchSlashCommand('/contextualbar'); await expect(poHomeChannel.btnContextualbarClose).toBeVisible(); }); test('expect app contextualbar to be closed', async () => { - await poHomeChannel.content.dispatchSlashCommand('/contextualbar'); await poHomeChannel.btnContextualbarClose.click(); await expect(poHomeChannel.btnContextualbarClose).toBeHidden(); }); diff --git a/apps/meteor/tests/e2e/apps/apps-modal.spec.ts b/apps/meteor/tests/e2e/apps/apps-modal.spec.ts new file mode 100644 index 0000000000000..53a4d479bb277 --- /dev/null +++ b/apps/meteor/tests/e2e/apps/apps-modal.spec.ts @@ -0,0 +1,49 @@ +import type { Page } from '@playwright/test'; + +import { Users } from '../fixtures/userStates'; +import { HomeChannel } from '../page-objects'; +import { Modal } from '../page-objects/modal'; +import { expect, test } from '../utils/test'; + +test.use({ storageState: Users.user1.state }); + +test.describe.serial('Apps > ContextualBar', () => { + let poHomeChannel: HomeChannel; + let poModal: Modal; + + let page: Page; + + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + + poHomeChannel = new HomeChannel(page); + poModal = new Modal(page); + + await page.goto('/home'); + await poHomeChannel.sidenav.openChat('general'); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('expect allow user open app modal', async () => { + await poHomeChannel.content.dispatchSlashCommand('/modal'); + await expect(poModal.btnModalSubmit).toBeVisible(); + }); + + test('expect validation error message appears in app modal', async () => { + await expect(poModal.textInput).toBeVisible(); + + await poModal.btnModalSubmit.click(); + + await expect(poModal.textInputErrorMessage).toBeVisible(); + }); + + test("expect validation error message don't appears in app modal", async () => { + await poModal.textInput.fill('something'); + await poModal.btnModalSubmit.click(); + + await expect(poModal.textInputErrorMessage).not.toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/fixtures/insert-apps.ts b/apps/meteor/tests/e2e/fixtures/insert-apps.ts index 17928896612fc..fd88714daa3ba 100644 --- a/apps/meteor/tests/e2e/fixtures/insert-apps.ts +++ b/apps/meteor/tests/e2e/fixtures/insert-apps.ts @@ -3,7 +3,7 @@ import { request } from '@playwright/test'; import { BASE_API_URL, BASE_URL } from '../config/constants'; import { Users } from './userStates'; -const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.5.zip?raw=true'; +const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.1.0.zip?raw=true'; export default async function insertApp(): Promise { const api = await request.newContext(); diff --git a/apps/meteor/tests/e2e/page-objects/modal.ts b/apps/meteor/tests/e2e/page-objects/modal.ts new file mode 100644 index 0000000000000..c8a66d71fa1c1 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/modal.ts @@ -0,0 +1,21 @@ +import type { Locator, Page } from '@playwright/test'; + +export class Modal { + protected readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get textInput(): Locator { + return this.page.locator('[name="modal_input"]'); + } + + get textInputErrorMessage(): Locator { + return this.page.getByText('Validation failed'); + } + + get btnModalSubmit(): Locator { + return this.page.locator('role=button[name="Submit"]'); + } +} diff --git a/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts b/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts index 11a7da5748592..8bd64348f772f 100644 --- a/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts +++ b/packages/fuselage-ui-kit/src/contexts/UiKitContext.ts @@ -26,7 +26,7 @@ type UiKitContextValue = { event: Parameters>[0] ) => Promise | void; appId?: string; - errors?: Record; + errors?: { [field: string]: string }[] | { [field: string]: string }; values: Record; viewId?: string; rid?: string; diff --git a/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts b/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts index 4468f4fd86a7d..6af9245f9a51a 100644 --- a/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts +++ b/packages/fuselage-ui-kit/src/hooks/useUiKitState.ts @@ -57,7 +57,11 @@ export const useUiKitState = ( const { values, errors } = useContext(UiKitContext); const _value = getElementValueFromState(actionId, values, initialValue); - const error = errors?.[actionId]; + const error = Array.isArray(errors) + ? errors.find((error) => + Object.keys(error).find((key) => key === actionId) + )?.[actionId] + : errors?.[actionId]; const [value, setValue] = useSafely(useState(_value)); const [loading, setLoading] = useSafely(useState(false)); From 703af95e657ebf623e89d83ef592bf91ec678f2f Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 22 Jul 2024 12:11:16 -0300 Subject: [PATCH 075/114] fix: Attachments not downloading on non-encrypted rooms (#32837) --- .changeset/hungry-wombats-act.md | 5 +++++ .../file/GenericFileAttachment.tsx | 20 ++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 .changeset/hungry-wombats-act.md diff --git a/.changeset/hungry-wombats-act.md b/.changeset/hungry-wombats-act.md new file mode 100644 index 0000000000000..4e50b172e17e7 --- /dev/null +++ b/.changeset/hungry-wombats-act.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed an issue where non-encrypted attachments were not being downloaded diff --git a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx index b7bcd7d1e9dd8..86223cd7f2b47 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -38,15 +38,21 @@ const GenericFileAttachment = ({ const { t } = useTranslation(); const handleTitleClick = (event: UIEvent): void => { - if (openDocumentViewer && link) { + if (!link) { + return; + } + + if (openDocumentViewer && format === 'PDF') { event.preventDefault(); - if (format === 'PDF') { - const url = new URL(getURL(link), window.location.origin); - url.searchParams.set('contentDisposition', 'inline'); - openDocumentViewer(url.toString(), format, ''); - return; - } + const url = new URL(getURL(link), window.location.origin); + url.searchParams.set('contentDisposition', 'inline'); + openDocumentViewer(url.toString(), format, ''); + return; + } + + if (link.includes('/file-decrypt/')) { + event.preventDefault(); registerDownloadForUid(uid, t, title); forAttachmentDownload(uid, link); From 5bfde8dc0e96ebc28d586a126cd74190aca9aafc Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Mon, 22 Jul 2024 14:37:16 -0300 Subject: [PATCH 076/114] feat: Improve UX from "Action Buttons" in `RoomInfo` (#32632) --- .changeset/weak-insects-sort.md | 5 + .../GenericMenu/GenericMenu.spec.tsx | 60 ++++++++ .../GenericMenu/GenericMenuItem.tsx | 1 + .../contextualBar/Info/RoomInfo/RoomInfo.tsx | 54 +++---- .../Info/RoomInfo/RoomInfoActions.tsx | 29 ++++ .../Info/hooks/useRoomActions.ts | 135 ++++++++++-------- .../Info/hooks/useSplitRoomActions.ts | 28 ++++ .../tests/e2e/channel-management.spec.ts | 9 +- .../fragments/home-flextab-room.ts | 8 +- 9 files changed, 232 insertions(+), 97 deletions(-) create mode 100644 .changeset/weak-insects-sort.md create mode 100644 apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfoActions.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/Info/hooks/useSplitRoomActions.ts diff --git a/.changeset/weak-insects-sort.md b/.changeset/weak-insects-sort.md new file mode 100644 index 0000000000000..cbbe7c4aa08c6 --- /dev/null +++ b/.changeset/weak-insects-sort.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Improving UX by change the position of room info actions buttons and menu order to avoid missclick in destructive actions. diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx b/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx new file mode 100644 index 0000000000000..99e62bac1a607 --- /dev/null +++ b/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx @@ -0,0 +1,60 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import GenericMenu from './GenericMenu'; + +const mockedFunction = jest.fn(); +const regular = { + items: [ + { + id: 'edit', + content: 'Edit', + icon: 'pencil' as const, + onClick: mockedFunction, + }, + ], +}; +const danger = { + items: [ + { + id: 'delete', + content: 'Delete', + icon: 'trash' as const, + onClick: () => null, + variant: 'danger', + }, + ], +}; + +const sections = [regular, danger]; + +describe('Room Actions Menu', () => { + it('should render kebab menu with the list content', async () => { + render(); + + userEvent.click(screen.getByRole('button')); + + expect(await screen.findByText('Edit')).toBeInTheDocument(); + expect(await screen.findByText('Delete')).toBeInTheDocument(); + }); + + it('should have two different sections, regular and danger', async () => { + render(); + + userEvent.click(screen.getByRole('button')); + + expect(screen.getAllByRole('presentation')).toHaveLength(2); + expect(screen.getByRole('separator')).toBeInTheDocument(); + }); + + it('should call the action when item clicked', async () => { + render(); + + userEvent.click(screen.getByRole('button')); + userEvent.click(screen.getAllByRole('menuitem')[0]); + + expect(mockedFunction).toHaveBeenCalled(); + }); +}); diff --git a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx b/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx index 44feedf861154..c01a64d708a02 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx +++ b/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx @@ -13,6 +13,7 @@ export type GenericMenuItemProps = { description?: ReactNode; gap?: boolean; tooltip?: string; + variant?: string; }; const GenericMenuItem = ({ icon, content, addon, status, gap, tooltip }: GenericMenuItemProps) => ( diff --git a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx index 678e5ec9f871a..b0bf1d083a717 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx @@ -1,8 +1,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { Box, Callout, Menu, Option } from '@rocket.chat/fuselage'; +import { Box, Callout, IconButton } from '@rocket.chat/fuselage'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; +import React from 'react'; import { ContextualbarHeader, @@ -12,9 +12,9 @@ import { ContextualbarClose, ContextualbarTitle, } from '../../../../../components/Contextualbar'; +import GenericMenu from '../../../../../components/GenericMenu/GenericMenu'; import { InfoPanel, - InfoPanelAction, InfoPanelActionGroup, InfoPanelAvatar, InfoPanelField, @@ -25,10 +25,10 @@ import { } from '../../../../../components/InfoPanel'; import RetentionPolicyCallout from '../../../../../components/InfoPanel/RetentionPolicyCallout'; import MarkdownText from '../../../../../components/MarkdownText'; -import type { Action } from '../../../../hooks/useActionSpread'; -import { useActionSpread } from '../../../../hooks/useActionSpread'; import { useRetentionPolicy } from '../../../hooks/useRetentionPolicy'; import { useRoomActions } from '../hooks/useRoomActions'; +import { useSplitRoomActions } from '../hooks/useSplitRoomActions'; +import RoomInfoActions from './RoomInfoActions'; type RoomInfoProps = { room: IRoom; @@ -47,35 +47,8 @@ const RoomInfo = ({ room, icon, onClickBack, onClickClose, onClickEnterRoom, onC const isDiscussion = 'prid' in room; const retentionPolicy = useRetentionPolicy(room); - const memoizedActions = useRoomActions(room, { onClickEnterRoom, onClickEdit }, resetState); - const { actions: actionsDefinition, menu: menuOptions } = useActionSpread(memoizedActions); - - const menu = useMemo(() => { - if (!menuOptions) { - return null; - } - - return ( -