// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {AxiosResponse} from 'axios'; import {ChainableT} from '../types'; /** * postMessageAs is a task which is wrapped as command with post-verification * that a message is successfully posted by the user/sender * @param {Object} sender - a user object who will post a message * @param {String} message - message in a post * @param {Object} channelId - where a post will be posted */ export interface PostMessageResp { id: string; status: number; data: any; } interface PostMessageArg { sender: { username: string; password: string; }; message: string; channelId: string; rootId?: string; createAt?: number; } interface PostMessageRequest { token: string; message: string; props?; channelId: string; rootId?; createAt?; failOnStatus?: boolean; } function postMessageAs(arg: PostMessageArg): ChainableT { const {sender, message, channelId, rootId, createAt} = arg; const baseUrl = Cypress.config('baseUrl'); return cy.task('postMessageAs', {sender, message, channelId, rootId, createAt, baseUrl}).then((response: AxiosResponse<{id: string}>) => { const {status, data} = response; expect(status).to.equal(201); // # Return the data so it can be interacted in a test return cy.wrap({id: data.id, status, data}); }); } Cypress.Commands.add('postMessageAs', postMessageAs); /** * @param {string} [numberOfMessages = 30] - Number of messages * @param {Object} sender - a user object who will post a message * @param {String} message - message in a post * @param {Object} channelId - where a post will be posted */ function postListOfMessages({numberOfMessages = 30, ...rest}): ChainableT { const baseUrl = Cypress.config('baseUrl'); return (cy as any). task('postListOfMessages', {numberOfMessages, baseUrl, ...rest}, {timeout: numberOfMessages * 200}). each((message) => expect(message.status).to.equal(201)); } Cypress.Commands.add('postListOfMessages', postListOfMessages); /** * reactToMessageAs is a task wrapped as command with post-verification * that a reaction is added successfully to a message by a user/sender * @param {Object} sender - a user object who will post a message * @param {String} postId - post on which reaction is intended * @param {String} reaction - emoji text eg. smile */ Cypress.Commands.add('reactToMessageAs', ({sender, postId, reaction}) => { const baseUrl = Cypress.config('baseUrl'); return cy.task('reactToMessageAs', {sender, postId, reaction, baseUrl}).then(({status, data}) => { expect(status).to.equal(200); // # Return the response after reaction is added return cy.wrap({status, data}); }); }); /** * postIncomingWebhook is a task which is wrapped as command with post-verification * that the incoming webhook is successfully posted * @param {String} url - incoming webhook URL * @param {Object} data - payload on incoming webhook */ function postIncomingWebhook({url, data, waitFor}: { url: string; data: Record; waitFor?: string; }): ChainableT { cy.task('postIncomingWebhook', {url, data}).its('status').should('be.equal', 200); if (!waitFor) { return; } cy.waitUntil(() => cy.getLastPost().then((el) => { switch (waitFor) { case 'text': { const textEl = el.find('.post-message__text > p')[0]; return Boolean(textEl && textEl.textContent.includes(data.text)); } case 'attachment-pretext': { const attachmentPretextEl = el.find('.attachment__thumb-pretext > p')[0]; return Boolean(attachmentPretextEl && attachmentPretextEl.textContent.includes(data.attachments[0].pretext)); } default: return false; } })); } Cypress.Commands.add('postIncomingWebhook', postIncomingWebhook); interface ExternalRequestArg { user: Record; method: string; path: string; data?: T; failOnStatusCode?: boolean; } function externalRequest(arg: ExternalRequestArg): ChainableT, 'data' | 'status'>> { const {user, method, path, data, failOnStatusCode = true} = arg; const baseUrl = Cypress.config('baseUrl'); return cy.task('externalRequest', {baseUrl, user, method, path, data}).then((response: Pick, 'data' | 'status'>) => { // Temporarily ignore error related to Cloud const cloudErrorId = [ 'ent.cloud.request_error', 'api.cloud.get_subscription.error', ]; if (response.data && !cloudErrorId.includes(response.data.id) && failOnStatusCode) { expect(response.status).to.be.oneOf([200, 201, 204]); } return cy.wrap(response); }); } Cypress.Commands.add('externalRequest', externalRequest); /** * postMessageAs is a task which is wrapped as command with post-verification * that a message is successfully posted by the bot * @param {String} message - message in a post * @param {Object} channelId - where a post will be posted */ function postBotMessage({token, message, props, channelId, rootId, createAt, failOnStatus = true}: PostMessageRequest): ChainableT { const baseUrl = Cypress.config('baseUrl'); return cy.task('postBotMessage', {token, message, props, channelId, rootId, createAt, baseUrl}).then(({status, data}) => { if (failOnStatus) { expect(status).to.equal(201); } // # Return the data so it can be interacted in a test return cy.wrap({id: data.id, status, data}); }); } Cypress.Commands.add('postBotMessage', postBotMessage); /** * urlHealthCheck is a task wrapped as command that checks whether * a URL is healthy and reachable. * @param {String} name - name of service to check * @param {String} url - URL to check * @param {String} helperMessage - a message to display on error to help resolve the issue * @param {String} method - a request using a specific method * @param {String} httpStatus - expected HTTP status */ function urlHealthCheck({name, url, helperMessage, method, httpStatus}): ChainableT { Cypress.log({name, message: `Checking URL health at ${url}`}); return cy.task('urlHealthCheck', {url, method}).then(({data, errorCode, status, success}) => { const urlService = `__${name}__ at ${url}`; const successMessage = success ? `${urlService}: reachable` : `${errorCode}: The test you're running requires ${urlService} to be reachable. \n${helperMessage}`; expect(success, successMessage).to.equal(true); const statusMessage = status === httpStatus ? `${urlService}: responded with ${status} HTTP status` : `${urlService}: expected to respond with ${httpStatus} but got ${status} HTTP status`; expect(status, statusMessage).to.equal(httpStatus); return cy.wrap({data, status}); }); } Cypress.Commands.add('urlHealthCheck', urlHealthCheck); Cypress.Commands.add('requireWebhookServer', () => { const baseUrl = Cypress.config('baseUrl'); const webhookBaseUrl = Cypress.env('webhookBaseUrl'); const adminUsername = Cypress.env('adminUsername'); const adminPassword = Cypress.env('adminPassword'); const helperMessage = ` __Tips:__ 1. In local development, you may run "__npm run start:webhook__" at "/e2e" folder. 2. If reachable from remote host, you may export it as env variable, like "__CYPRESS_webhookBaseUrl=[url] npm run cypress:open__". `; cy.urlHealthCheck({ name: 'Webhook Server', url: webhookBaseUrl, helperMessage, method: 'get', httpStatus: 200, }); cy.task('postIncomingWebhook', { url: `${webhookBaseUrl}/setup`, data: { baseUrl, webhookBaseUrl, adminUsername, adminPassword, }}). its('status').should('be.equal', 201); }); Cypress.Commands.overwrite('log', (subject, message) => cy.task('log', message)); declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { /** * externalRequest is a task which is wrapped as command with post-verification * that the external request is successfully completed * @param {Object} options * @param {} options.user - a user initiating external request * @param {String} options.method - an HTTP method (e.g. get, post, etc) * @param {String} options.path - API path that is relative to Cypress.config().baseUrl * @param {Object} options.data - payload * @param {Boolean} options.failOnStatusCode - whether to fail on status code, default is true * * @example * cy.externalRequest({user: sysadmin, method: 'POST', path: 'config', data}); */ externalRequest(options?: { user: Pick; method: string; path: string; data?: Record; failOnStatusCode?: boolean; }): Chainable; /** * Adds a given reaction to a specific post from a user * @param {Object} reactToMessageObject - Information on person and post to which a reaction needs to be added * @param {Object} reactToMessageObject.sender - a user object who will post a message * @param {string} reactToMessageObject.postId - post on which reaction is intended * @param {string} reactToMessageObject.reaction - emoji text eg. smile * @returns {Response} response: Cypress-chainable response * * @example * cy.reactToMessageAs({sender:user2, postId:"ABC123", reaction: 'smile'}); */ reactToMessageAs({sender, postId, reaction}: {sender: Record; postId: string; reaction: string}): Chainable; /** * Verify that the webhook server is accessible, and then sets up base URLs and credential. * * @example * cy.requireWebhookServer(); */ requireWebhookServer(): Chainable; postMessageAs: typeof postMessageAs; postListOfMessages: typeof postListOfMessages; postIncomingWebhook: typeof postIncomingWebhook; postBotMessage: typeof postBotMessage; urlHealthCheck: typeof urlHealthCheck; } } }