├── .ci └── node10 │ └── Dockerfile.linux ├── .circleci └── config.yml ├── .cirrus.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .template-lintrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── config └── default.js ├── db ├── channels.json ├── index.js ├── messages │ ├── C12345678.json │ ├── C12345679.json │ └── index.js ├── sessions.json ├── teams.json └── users.json ├── docs ├── ABOUT_RU.md ├── CODECLIMATE.md ├── DOCKER.md ├── FOR_DEVELOPERS_EN.md ├── FOR_DEVELOPERS_RU.md ├── NPMCOMMANDS.md ├── ROADMAP.md └── images │ ├── demo.gif │ └── logo.png ├── examples └── rtmbot │ ├── __tests__ │ └── integration │ │ ├── actions.js │ │ ├── channel.test.js │ │ ├── direct.test.js │ │ ├── greeting.test.js │ │ ├── shared.js │ │ └── utils.js │ └── index.js ├── features ├── auth_test_api.feature ├── bold_message_formatting.feature ├── breaks_at_the_any_position_of_message.feature ├── code_message_formatting.feature ├── edit_last_message.feature ├── hide_message_header.feature ├── incoming_message_payload.feature ├── incoming_user_typing_payload.feature ├── inline_message_editor_linebreaks.feature ├── italic_message_formatting.feature ├── multiline_messages.feature ├── pagecontent.feature ├── preformatted_message_formatting.feature ├── quote_message_formatting.feature ├── receiving_messages.feature ├── sending_messages.feature ├── step_definitions │ ├── actions.js │ ├── hooks.js │ ├── pages.js │ ├── selectors.js │ ├── steps.js │ ├── support │ │ ├── fakeuser.js │ │ ├── scope.js │ │ ├── services.js │ │ └── validators.js │ └── world.js ├── strike_message_formatting.feature └── update_message_event.feature ├── helpers.js ├── jest-puppeteer.config.js ├── package-lock.json ├── package.json ├── public ├── css │ └── iconmonstr-iconic-font.min.css ├── fonts │ ├── iconmonstr-iconic-font.eot │ ├── iconmonstr-iconic-font.ttf │ ├── iconmonstr-iconic-font.woff │ └── iconmonstr-iconic-font.woff2 ├── images │ ├── loading_channel_pane@2x.png │ ├── meavatar.png │ ├── slack_user.png │ └── uslackbot-avatar-72.png └── js │ ├── formatters.js │ └── helpers.js ├── reports └── .keep ├── routes ├── api │ ├── factories.js │ ├── index.js │ └── utils.js ├── app │ └── index.js ├── constants.js ├── managers.js ├── responses │ ├── auth.test.json │ ├── cant_update_message.json │ ├── channels.list.json │ ├── chat.postMessage.json │ ├── index.js │ ├── invalid_auth.json │ ├── rtm.connect.json │ ├── team.info.json │ ├── user_not_found.json │ └── users.info.json ├── rtm │ └── index.js └── testapi │ └── index.js ├── screenshots └── .keep ├── scripts ├── codeclimate │ ├── check.js │ └── setup.sh ├── glitch.js ├── ntimes.sh └── volumes │ └── inspect.js ├── server.js └── views ├── layouts └── main.hbs ├── partials ├── appitem.hbs ├── appitemheader.hbs ├── channel.hbs ├── channelheader.hbs ├── directuser.hbs ├── inlinestyles.hbs ├── invite.hbs ├── mehref.hbs ├── message.hbs ├── message_edit.hbs ├── statustyping.hbs ├── submitform.hbs └── userchannelheader.hbs └── slackv2.hbs /.ci/node10/Dockerfile.linux: -------------------------------------------------------------------------------- 1 | FROM node:10.16.0 2 | 3 | RUN apt-get update && \ 4 | apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ 5 | libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ 6 | libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ 7 | libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ 8 | libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget && \ 9 | rm -rf /var/lib/apt/lists/* 10 | 11 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 12 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 13 | && apt-get update \ 14 | && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ 15 | --no-install-recommends \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | # Add user so we don't need --no-sandbox. 19 | RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ 20 | && mkdir -p /home/pptruser/Downloads \ 21 | && chown -R pptruser:pptruser /home/pptruser 22 | 23 | # Run everything after as non-privileged user. 24 | USER pptruser -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10.16.0 6 | working_directory: ~/mad-fake-slack 7 | steps: 8 | - checkout 9 | # Download and cache dependencies 10 | - restore_cache: 11 | keys: 12 | - v1-dependencies-{{ checksum "package.json" }} 13 | # fallback to using the latest cache if no exact match is found 14 | - v1-dependencies- 15 | - run: 16 | name: Install dependencies 17 | command: | 18 | sudo apt-get update 19 | sudo apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ 20 | libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ 21 | libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ 22 | libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ 23 | ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget --fix-missing 24 | - run: #STABLE 25 | name: Install Chromedriver latest version 26 | command: | 27 | sudo apt-get update 28 | sudo apt-get install lsb-release libappindicator3-1 29 | curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 30 | sudo dpkg -i google-chrome.deb 31 | sudo sed -i 's|HERE/chrome"|HERE/chrome" --no-sandbox|g' /opt/google/chrome/google-chrome 32 | rm google-chrome.deb 33 | - run: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm i 34 | - save_cache: 35 | paths: 36 | - node_modules 37 | key: v1-dependencies-{{ checksum "package.json" }} 38 | - run: npm test 39 | - store_artifacts: 40 | path: ~/mad-fake-slack/screenshots 41 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | env: 2 | DISPLAY: :99.0 3 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true 4 | USE_SANDBOX: true 5 | 6 | task: 7 | matrix: 8 | - name: Chromium (node10 + linux) 9 | container: 10 | dockerfile: .ci/node10/Dockerfile.linux 11 | xvfb_start_background_script: Xvfb :99 -ac -screen 0 1920x1080x24 12 | install_script: npm install --unsafe-perm 13 | test_script: npm test -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 4 | #------------------------------------------------------------------------------------------------------------- 5 | 6 | FROM node:lts 7 | 8 | # Configure apt 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | RUN apt-get update \ 11 | && apt-get -y install --no-install-recommends apt-utils 2>&1 12 | 13 | # Install and configure locales 14 | RUN apt-get install -y locales 15 | 16 | RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ 17 | dpkg-reconfigure --frontend=noninteractive locales && \ 18 | update-locale LANG=en_US.UTF-8 19 | 20 | ENV LANG en_US.UTF-8 21 | 22 | # Verify git and needed tools are installed 23 | RUN apt-get install -y git procps 24 | 25 | # Remove outdated yarn from /opt and install via package 26 | # so it can be easily updated via apt-get upgrade yarn 27 | RUN rm -rf /opt/yarn-* \ 28 | && rm -f /usr/local/bin/yarn \ 29 | && rm -f /usr/local/bin/yarnpkg \ 30 | && apt-get install -y curl apt-transport-https lsb-release \ 31 | && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ 32 | && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 33 | && apt-get update \ 34 | && apt-get -y install --no-install-recommends yarn 35 | 36 | # Install eslint 37 | RUN npm install -g eslint 38 | 39 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 40 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 41 | && apt-get update \ 42 | && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ 43 | --no-install-recommends \ 44 | && rm -rf /var/lib/apt/lists/* 45 | 46 | RUN apt-get update \ 47 | && apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common lsb-release \ 48 | && curl -fsSL https://download.docker.com/linux/$(/usr/bin/lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | apt-key add - 2>&1 >/dev/null \ 49 | && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(/usr/bin/lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" \ 50 | && apt-get update \ 51 | && apt-get install -y docker-ce-cli 52 | 53 | # Uncomment to skip the chromium download when installing puppeteer. If you do, 54 | # you'll need to launch puppeteer with: 55 | # browser.launch({executablePath: 'google-chrome-stable'}) 56 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true 57 | 58 | # Clean up 59 | RUN apt-get autoremove -y \ 60 | && apt-get clean -y \ 61 | && rm -rf /var/lib/apt/lists/* 62 | ENV DEBIAN_FRONTEND=dialog 63 | 64 | VOLUME [ "/root/.vscode-server-insiders" ] 65 | 66 | # Set the default shell to bash instead of sh 67 | ENV SHELL /bin/bash -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "name": "Node.js (latest LTS)", 4 | "dockerFile": "Dockerfile", 5 | 6 | // Uncomment the next line if you want to publish any ports. 7 | "appPort": [9001, 9222], 8 | "runArgs": [ 9 | "-v", "/var/run/docker.sock:/var/run/docker.sock", 10 | "-e", "CONTAINER_TIMEOUT_SECONDS=1800" 11 | ], 12 | // Uncomment the next line if you want to add in default container specific settings.json values 13 | // "settings": { "workbench.colorTheme": "Quiet Light" }, 14 | 15 | // Uncomment the next line to run commands after the container is created. 16 | "postCreateCommand": "npm i && npm run codeclimate:install", 17 | 18 | "extensions": [ 19 | "dbaeumer.vscode-eslint", 20 | "editorconfig.editorconfig", 21 | "ms-vscode.atom-keybindings", 22 | "alexkrechik.cucumberautocomplete", 23 | "ms-azuretools.vscode-docker", 24 | "stevejpurves.cucumber" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.hbs] 12 | charset = utf-8 13 | end_of_line = lf 14 | indent_size = 2 15 | indent_style = space 16 | insert_final_newline = false 17 | trim_trailing_whitespace = true 18 | 19 | [vcbuild.bat] 20 | end_of_line = crlf 21 | 22 | [Makefile] 23 | indent_size = 8 24 | indent_style = tab 25 | 26 | [{deps}/**] 27 | charset = ignore 28 | end_of_line = ignore 29 | indent_size = ignore 30 | indent_style = ignore 31 | trim_trailing_whitespace = ignore 32 | 33 | [{test/fixtures,deps,tools/node_modules,tools/gyp,tools/icu,tools/msvs}/**] 34 | insert_final_newline = false 35 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": ["airbnb-base/legacy"], 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "plugins": ["jest"], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly", 13 | "page": true, 14 | "browser": true, 15 | "context": true, 16 | "jestPuppeteer": true, 17 | }, 18 | "parserOptions": { 19 | "ecmaVersion": 2018, 20 | "sourceType": "module" 21 | }, 22 | "rules": { 23 | "jest/no-disabled-tests": "warn", 24 | "jest/no-focused-tests": "error", 25 | "jest/no-identical-title": "error", 26 | "jest/prefer-to-have-length": "warn", 27 | "jest/valid-expect": "error", 28 | "no-restricted-syntax": "warn", 29 | "max-len": [ 30 | 2, 31 | 180, 32 | 8 33 | ], 34 | "linebreak-style": 0, 35 | "eslint linebreak-style": [0, "error", "windows"], 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .DS_Store 64 | 65 | reports/* 66 | !reports/.keep 67 | 68 | screenshots/* 69 | !screenshots/.keep -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'recommended', 3 | ignore: [ 4 | 'views/layouts/main', 5 | 'views/slackv2' 6 | ], 7 | rules: { 8 | 'no-bare-strings': false, 9 | 'no-triple-curlies': false, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: xenial 3 | addons: 4 | chrome: stable 5 | notifications: 6 | email: false 7 | cache: 8 | directories: 9 | - node_modules 10 | sudo: required 11 | node_js: 12 | - "10.15.3" 13 | services: 14 | - xvfb 15 | env: 16 | global: 17 | - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 18 | # allow headful tests 19 | before_install: 20 | # Enable user namespace cloning 21 | - "sysctl kernel.unprivileged_userns_clone=1" 22 | install: 23 | - npm i 24 | script: 25 | - npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mad Devs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | mad-fake-slack 5 |
6 |
7 |

Your fake service #1!

8 |
9 |
10 | 11 | [![Developed by Mad Devs](https://mdbadge.glitch.me/badge.svg?theme=red-white)](https://maddevs.io) 12 | [![Build Status](https://api.cirrus-ci.com/github/maddevsio/mad-fake-slack.svg)](https://cirrus-ci.com/github/maddevsio/mad-fake-slack) 13 | [![CircleCI](https://circleci.com/gh/maddevsio/mad-fake-slack.svg?style=svg)](https://circleci.com/gh/maddevsio/mad-fake-slack) 14 | [![Build Status](https://travis-ci.org/maddevsio/mad-fake-slack.svg?branch=master)](https://travis-ci.org/maddevsio/mad-fake-slack) 15 | 16 | ### About project ([`RU`](docs/ABOUT_RU.md)) 17 | This project is designed to help test your Slack bots in isolation from the actual Slack service. This approach allows you to run tests on CI and simulate various situations with data in the chat. 18 | The project consists of two parts: user interface and API. 19 | All communication of your bot is carried out through API methods identical to those described in the Slack API documentation. Server side is written in node.js. 20 | 21 | ### Highlights 22 | * [Demo mad-fake-slack + example bot](https://mad-fake-slack.glitch.me) 23 | * [UI feaures](#the_user_interface_gives_you_the_ability_to) 24 | * [API features](#the-api-gives-you-the-ability-to) 25 | * [For developers](#for-developers-en) 26 | * [Для разработчиков](#%D0%B4%D0%BB%D1%8F-%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%87%D0%B8%D0%BA%D0%BE%D0%B2-ru) 27 | * [DOCKER IMAGE | Докер образ](#docker-image--%D0%B4%D0%BE%D0%BA%D0%B5%D1%80-%D0%BE%D0%B1%D1%80%D0%B0%D0%B7) 28 | * [ROADMAP | Путь развития](#roadmap--%D0%BF%D1%83%D1%82%D1%8C-%D1%80%D0%B0%D0%B7%D0%B2%D0%B8%D1%82%D0%B8%D1%8F) 29 | 30 | ### Demo of interaction with bot from examples folder (Демо взаимодействия с ботом, через UI `mad-fake-slack`) 31 |
32 | 33 |
34 | 35 | #### The user interface gives you the ability to: 36 | * See current chat situation 37 | * Sending text messages using simple formatting `*bold*`, `~strike~`, ``` `code` ```, ` ```preformatted``` `, `>quote` 38 | * Send messages to different channels, as well as view messages on these channels 39 | * Writing tests using the Gherkin syntax and any library you prefer (cucumber, cucumber.js, etc.). Your tests can interact with the user interface and perform user manipulations to test the functionality of the bot. 40 | * Observe receipt of `user_typing` messages, under the message input field. 41 | 42 | #### The API gives you the ability to: 43 | * Using a token for authentication in mad-fake-slack, as in real Slack. 44 | * Sending text messages using simple formatting `*bold*`, `~strike~`, ``` `code` ```, ` ```preformatted``` `, `>quote` 45 | * Request a list of channels with their identifiers 46 | * Sending messages to existing channels via HTTP and RTM 47 | * Receive messages from existing channels (via RTM). 48 | * Receive / send `typing` or `user_typing` messages (via RTM) 49 | * User information request 50 | 51 | ### Badges 52 | [![Dependabot](https://badgen.net/badge/Dependabot/enabled/blue?icon=dependabot)](https://dependabot.com/) 53 | [![Maintainability](https://api.codeclimate.com/v1/badges/684a8d656c2148c12850/maintainability)](https://codeclimate.com/github/maddevsio/mad-fake-slack/maintainability) 54 | [![dependencies Status](https://david-dm.org/maddevsio/mad-fake-slack.svg)](https://david-dm.org/maddevsio/mad-fake-slack) 55 | [![devDependencies Status](https://david-dm.org/maddevsio/mad-fake-slack/dev-status.svg)](https://david-dm.org/maddevsio/mad-fake-slack?type=dev) 56 | [![Known Vulnerabilities](https://snyk.io/test/github/maddevsio/mad-fake-slack/badge.svg)](https://snyk.io/test/github/maddevsio/mad-fake-slack) 57 | 58 | ### For Developers `EN` 59 | * Read [here](docs/FOR_DEVELOPERS_EN.md) 60 | 61 | ### Для разработчиков `RU` 62 | * Читайте [тут](docs/FOR_DEVELOPERS_RU.md) 63 | 64 | ### [DOCKER IMAGE | Докер образ](docs/DOCKER.md) 65 | * [EN] coming soon... 66 | * [RU] скоро будет... 67 | 68 | ### [ROADMAP | Путь развития](docs/ROADMAP.md) 69 | * [EN] coming soon... 70 | * [RU] скоро будет... 71 | 72 | ### [LICENSE | Лицензия](LICENSE) 73 | -------------------------------------------------------------------------------- /config/default.js: -------------------------------------------------------------------------------- 1 | function isPresent(variable) { 2 | return typeof variable !== 'undefined' && variable !== null && variable !== ''; 3 | } 4 | 5 | module.exports = function configure() { 6 | if (!isPresent(process.env.PORT)) { 7 | process.env.PORT = 9001; 8 | } 9 | if (!isPresent(process.env.HOST)) { 10 | process.env.HOST = '0.0.0.0'; 11 | } 12 | if (!isPresent(process.env.DEFAULT_HEADER_HIDE_TIME_INTERVAL_IN_MIN)) { 13 | process.env.DEFAULT_HEADER_HIDE_TIME_INTERVAL_IN_MIN = 5; 14 | } 15 | if (!isPresent(process.env.MESSAGES_MAX_COUNT)) { 16 | process.env.MESSAGES_MAX_COUNT = 100; 17 | } 18 | if (!isPresent(process.env.MAIN_PAGE)) { 19 | process.env.MAIN_PAGE = 'slackv2'; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /db/channels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "C12345679", 4 | "name": "general", 5 | "is_channel": true, 6 | "is_group": false, 7 | "is_im": false, 8 | "created": 1449709280, 9 | "creator": "W12345678", 10 | "is_archived": false, 11 | "is_general": true, 12 | "unlinked": 0, 13 | "name_normalized": "general", 14 | "is_read_only": false, 15 | "is_shared": false, 16 | "is_ext_shared": false, 17 | "is_org_shared": false, 18 | "pending_shared": [], 19 | "is_pending_ext_shared": false, 20 | "is_member": true, 21 | "is_private": false, 22 | "is_mpim": false, 23 | "last_read": "1502126650.228446", 24 | "members": [ 25 | "W12345678", 26 | "W12345679" 27 | ], 28 | "topic": { 29 | "value": "Talk about anything!", 30 | "creator": "W12345678", 31 | "last_set": 1449709364 32 | }, 33 | "purpose": { 34 | "value": "To talk about anything!", 35 | "creator": "W12345678", 36 | "last_set": 1449709334 37 | }, 38 | "previous_names": [], 39 | "num_members": 2, 40 | "locale": "en-US" 41 | }, 42 | { 43 | "id": "C12345678", 44 | "name": "random", 45 | "is_channel": true, 46 | "is_group": false, 47 | "is_im": false, 48 | "created": 1449709280, 49 | "creator": "W12345678", 50 | "is_archived": false, 51 | "is_general": false, 52 | "unlinked": 0, 53 | "name_normalized": "random", 54 | "is_read_only": false, 55 | "is_shared": false, 56 | "is_ext_shared": false, 57 | "is_org_shared": false, 58 | "pending_shared": [], 59 | "is_pending_ext_shared": false, 60 | "is_member": true, 61 | "is_private": false, 62 | "is_mpim": false, 63 | "last_read": "1502126650.228446", 64 | "members": [ 65 | "W12345678", 66 | "W12345679" 67 | ], 68 | "topic": { 69 | "value": "Other stuff", 70 | "creator": "W12345678", 71 | "last_set": 1449709352 72 | }, 73 | "purpose": { 74 | "value": "A place for non-work-related flimflam, faffing, hodge-podge or jibber-jabber you'd prefer to keep out of more focused work-related channels.", 75 | "creator": "", 76 | "last_set": 0 77 | }, 78 | "previous_names": [], 79 | "num_members": 2, 80 | "locale": "en-US" 81 | } 82 | ] -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const helpers = require('../helpers'); 4 | 5 | class DbReader { 6 | constructor(dirname) { 7 | this.dirname = dirname; 8 | } 9 | 10 | static makeCopy(data) { 11 | return JSON.parse(JSON.stringify(data)); 12 | } 13 | 14 | read() { 15 | const db = {}; 16 | fs.readdirSync(this.dirname).forEach(file => { 17 | if (file.match(/\.json$/) !== null && file !== 'index.js') { 18 | const name = file.replace('.json', ''); 19 | db[name] = JSON.parse(fs.readFileSync(path.join(this.dirname, file))); 20 | } 21 | }); 22 | // eslint-disable-next-line global-require 23 | const messages = require('./messages'); 24 | db.messages = messages; 25 | return DbReader.makeCopy(db); 26 | } 27 | } 28 | 29 | class DbManager { 30 | constructor(dbReader, helpersObj) { 31 | this.db = null; 32 | this.dbReader = dbReader; 33 | this.helpers = helpersObj; 34 | this.initDb(); 35 | } 36 | 37 | initDb() { 38 | if (!this.db) { 39 | this.db = this.dbReader.read(); 40 | } else { 41 | throw new Error('DbManager: Database already initialized'); 42 | } 43 | } 44 | 45 | checkIsDbInitialized() { 46 | if (!this.db) throw new Error('DbManager: Database not initialized! Please call `initDb() method`'); 47 | } 48 | 49 | initMessages(channelId) { 50 | if (!this.db.messages[channelId]) { 51 | this.db.messages[channelId] = { 52 | meta: { 53 | last_id: 0 54 | } 55 | }; 56 | } 57 | } 58 | 59 | reset() { 60 | this.db = null; 61 | this.initDb(); 62 | } 63 | 64 | static createTs(id) { 65 | const dateNow = Date.now(); 66 | return `${Math.floor(dateNow / 1000)}.${String(id).padStart(6, '0')}`; 67 | } 68 | 69 | slackUser() { 70 | this.checkIsDbInitialized(); 71 | return this.db.users[0]; 72 | } 73 | 74 | slackTeam() { 75 | this.checkIsDbInitialized(); 76 | return this.db.teams.filter(t => t.id === this.slackUser().team_id)[0]; 77 | } 78 | 79 | users() { 80 | return { 81 | findById: (uid, where = () => true) => { 82 | return this.db.users.filter(u => u.id === uid && where(u)); 83 | } 84 | }; 85 | } 86 | 87 | teams() { 88 | return { 89 | findById: (tid, where = () => true) => { 90 | return this.db.teams.filter(tm => tm.id === tid && where(tm)); 91 | } 92 | }; 93 | } 94 | 95 | channel(channelId) { 96 | this.checkIsDbInitialized(); 97 | return { 98 | createMessage: (userId, message) => { 99 | this.initMessages(channelId); 100 | const messages = this.db.messages[channelId]; 101 | messages.meta.last_id += 1; 102 | 103 | let id; 104 | if (message.ts && !messages[message.ts]) { 105 | id = message.ts; 106 | } else { 107 | id = DbManager.createTs(messages.meta.last_id); 108 | } 109 | 110 | messages[id] = { 111 | type: 'message', 112 | user_id: userId, 113 | text: message.text, 114 | ts: id 115 | }; 116 | return messages[id]; 117 | }, 118 | findMessageByTs: ts => { 119 | this.initMessages(channelId); 120 | const message = this.db.messages[channelId][ts]; 121 | if (message) { 122 | const user = this.db.users.filter(u => u.id === message.user_id)[0]; 123 | const team = this.db.teams.filter(t => t.id === user.team_id)[0]; 124 | return { 125 | ...message, 126 | user, 127 | team 128 | }; 129 | } 130 | return message; 131 | }, 132 | updateMessage: message => { 133 | this.initMessages(channelId); 134 | if (!this.db.messages[channelId][message.ts]) { 135 | throw new Error('Message not found'); 136 | } 137 | this.db.messages[channelId][message.ts] = { 138 | ...message, 139 | edited: true 140 | }; 141 | return this.db.messages[channelId][message.ts]; 142 | }, 143 | messages: total => { 144 | this.initMessages(channelId); 145 | return Object.values(this.db.messages[channelId]) 146 | .slice(1) 147 | .slice(Number.isInteger(total) ? -total : total) 148 | .map(m => { 149 | const user = this.db.users.filter(u => u.id === m.user_id)[0]; 150 | const team = this.db.teams.filter(t => t.id === user.team_id)[0]; 151 | return { 152 | ...m, 153 | user, 154 | team 155 | }; 156 | }); 157 | } 158 | }; 159 | } 160 | } 161 | 162 | function createDbManager() { 163 | return new DbManager( 164 | new DbReader(__dirname), 165 | helpers 166 | ); 167 | } 168 | 169 | module.exports = { 170 | DbManager, 171 | DbReader, 172 | createDbManager 173 | }; 174 | -------------------------------------------------------------------------------- /db/messages/C12345678.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "last_id": 4 4 | }, 5 | "1560402102.000001": { 6 | "type": "message", 7 | "user_id": "W12345678", 8 | "text": "Lorem ipsum dolor sit amet, at vivendo lobortis nec. His in sale menandri petentium, mea tollit mentitum eu, autem ignota pro no. Eu mea prima ceteros, diceret facilis pericula ne eam. At has lorem mentitum. Ne ullum aeterno vel. Te commodo laoreet nominati sit. Eius iriure ad cum. Mea ad quis delectus officiis, his sint laboramus ne. An doming docendi omittantur has. Congue deseruisse at vix. Id commune consulatu molestiae vis, vix tollit commune ad", 9 | "ts": "1560402102.000001" 10 | }, 11 | "1560402103.000002": { 12 | "type": "message", 13 | "user_id": "W12345679", 14 | "text": "A robot must protect its own existence as long as such protection does not conflict with the First or Second Laws.", 15 | "ts": "1560402103.000002" 16 | }, 17 | "1560402106.000003": { 18 | "type": "message", 19 | "user_id": "W12345679", 20 | "text": "A robot must obey the orders given it by human beings except where such orders would conflict with the First Law", 21 | "ts": "1560402106.000003" 22 | }, 23 | "1560402110.000004": { 24 | "type": "message", 25 | "user_id": "W12345679", 26 | "text": "A robot may not injure a human being or, through inaction, allow a human being to come to harm.", 27 | "ts": "1560402110.000004" 28 | } 29 | } -------------------------------------------------------------------------------- /db/messages/C12345679.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "last_id": 0 4 | } 5 | } -------------------------------------------------------------------------------- /db/messages/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const modules = {}; 4 | require('fs').readdirSync(__dirname).forEach((file) => { 5 | if (file.match(/\.json$/) !== null && file !== 'index.js') { 6 | const name = file.replace('.json', ''); 7 | modules[name] = JSON.parse(fs.readFileSync(path.join(__dirname, file))); 8 | } 9 | }); 10 | 11 | module.exports = modules; 12 | -------------------------------------------------------------------------------- /db/sessions.json: -------------------------------------------------------------------------------- 1 | {"f643418abf2af667a96598f51b1cd9d1": "W12345679"} -------------------------------------------------------------------------------- /db/teams.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "T12345678", 4 | "name": "BotFactory", 5 | "domain": "localhost:9001", 6 | "email_domain": "localhost:9001", 7 | "icon": { 8 | "image_34": "/images/slack_user.png", 9 | "image_44": "/images/slack_user.png", 10 | "image_68": "/images/slack_user.png", 11 | "image_88": "/images/slack_user.png", 12 | "image_102": "/images/slack_user.png", 13 | "image_132": "/images/slack_user.png", 14 | "image_default": true 15 | }, 16 | "enterprise_id": "E123456789", 17 | "enterprise_name": "Umbrella Corporation" 18 | } 19 | ] -------------------------------------------------------------------------------- /db/users.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "W12345678", 3 | "team_id": "T12345678", 4 | "name": "valera.petrov", 5 | "deleted": false, 6 | "color": "9f69e7", 7 | "real_name": "Valera Petrov", 8 | "tz": "Asia/Dhaka", 9 | "tz_label": "Bangladesh Standard Time", 10 | "tz_offset": 21600, 11 | "profile": { 12 | "avatar_hash": "ge3b51ca72de", 13 | "status_text": "Print is dead", 14 | "status_emoji": ":books:", 15 | "real_name": "Valera Petrov", 16 | "display_name": "valera.petrov", 17 | "real_name_normalized": "Valera Petrov", 18 | "display_name_normalized": "valera.petrov", 19 | "email": "petrov@ghostbusters.example.com", 20 | "image_original": "/images/meavatar.png", 21 | "image_24": "/images/meavatar.png", 22 | "image_32": "/images/meavatar.png", 23 | "image_48": "/images/meavatar.png", 24 | "image_72": "/images/meavatar.png", 25 | "image_192": "/images/meavatar.png", 26 | "image_512": "/images/meavatar.png", 27 | "team": "T12345678" 28 | }, 29 | "is_admin": true, 30 | "is_owner": false, 31 | "is_primary_owner": false, 32 | "is_restricted": false, 33 | "is_ultra_restricted": false, 34 | "is_bot": false, 35 | "updated": 1502138686, 36 | "is_app_user": false, 37 | "has_2fa": false 38 | }, 39 | { 40 | "id": "USLACKBOT", 41 | "team_id": "T12345678", 42 | "name": "slackbot", 43 | "deleted": false, 44 | "color": "757575", 45 | "real_name": "Slackbot", 46 | "tz": null, 47 | "tz_label": "Pacific Daylight Time", 48 | "tz_offset": -25200, 49 | "profile": { 50 | "title": "", 51 | "phone": "", 52 | "skype": "", 53 | "real_name": "Slackbot", 54 | "real_name_normalized": "Slackbot", 55 | "display_name": "Slackbot", 56 | "display_name_normalized": "Slackbot", 57 | "fields": null, 58 | "status_text": "", 59 | "status_emoji": "", 60 | "status_expiration": 0, 61 | "avatar_hash": "nv00c8ed12a0", 62 | "always_active": true, 63 | "first_name": "slackbot", 64 | "last_name": "", 65 | "status_text_canonical": "", 66 | "team": "T12345678" 67 | }, 68 | "is_admin": false, 69 | "is_owner": false, 70 | "is_primary_owner": false, 71 | "is_restricted": false, 72 | "is_ultra_restricted": false, 73 | "is_bot": false, 74 | "is_app_user": false, 75 | "updated": 0, 76 | "presence": "active" 77 | }, 78 | { 79 | "id": "W12345679", 80 | "team_id": "T12345678", 81 | "name": "valera", 82 | "deleted": false, 83 | "color": "9f69e7", 84 | "real_name": "Valera", 85 | "tz": "Asia/Dhaka", 86 | "tz_label": "Bangladesh Standard Time", 87 | "tz_offset": 21600, 88 | "profile": { 89 | "avatar_hash": "ge3b51ca72de", 90 | "status_text": "Print is dead", 91 | "status_emoji": ":books:", 92 | "real_name": "Valera", 93 | "display_name": "spengler", 94 | "real_name_normalized": "Valera", 95 | "display_name_normalized": "valera", 96 | "email": "petrov.bot@ghostbusters.example.com", 97 | "image_original": "/images/slack_user.png", 98 | "image_24": "/images/slack_user.png", 99 | "image_32": "/images/slack_user.png", 100 | "image_48": "/images/slack_user.png", 101 | "image_72": "/images/slack_user.png", 102 | "image_192": "/images/slack_user.png", 103 | "image_512": "/images/slack_user.png", 104 | "team": "T12345678" 105 | }, 106 | "is_admin": false, 107 | "is_owner": false, 108 | "is_primary_owner": false, 109 | "is_restricted": false, 110 | "is_ultra_restricted": false, 111 | "is_bot": true, 112 | "updated": 1502138686, 113 | "is_app_user": true, 114 | "has_2fa": false 115 | }] -------------------------------------------------------------------------------- /docs/ABOUT_RU.md: -------------------------------------------------------------------------------- 1 | ### О проекте 2 | Этот проект призван помочь в тестировании ваших Slack-ботов в отрыве от реального сервиса Slack. Такой подход позволяет запускать тесты на CI и моделировать различные ситуации с данными в чате. 3 | Проект состоит из двух частей: пользовательского интерфейса и API. 4 | Все общение вашего бота осуществляется через методы API, идентичные описанным в документации Slack API. На стороне сервера написано в node.js. 5 | 6 | Пользовательский интерфейс дает вам возможность: 7 | * Смотрите текущую ситуацию в чате 8 | * Отправка текстовых сообщений с использованием простого форматирования (* полужирный * ~ strike ~ `code```` preformatted```> quote). 9 | * Отправлять сообщения на разные каналы, а также просматривать сообщения на этих каналах 10 | * Написание тестов с использованием синтаксиса Gherkin и любой библиотеки, которую вы предпочитаете (cucumber, cucumber.js и т. Д.). Ваши тесты могут взаимодействовать с пользовательским интерфейсом и выполнять пользовательские манипуляции, чтобы проверить функциональность бота. 11 | * Наблюдать получение user_typing сообщений, под полем ввода сообщения. 12 | 13 | API дает вам возможность: 14 | * Использование токена для аутентификации в mad-fake-slack, как в реальном Slack. 15 | * Отправка текстовых сообщений с использованием простого форматирования (* полужирный * ~ strike ~ `code```` preformatted```> quote). 16 | * Запросить список каналов с их идентификаторами 17 | * Отправка сообщений на существующие каналы через HTTP и RTM 18 | * Получать сообщения с существующих каналов (через RTM). 19 | * Получение / отправка `печатающих` сообщений (через RTM) 20 | * Запрос информации о пользователе -------------------------------------------------------------------------------- /docs/CODECLIMATE.md: -------------------------------------------------------------------------------- 1 | ## `EN` 2 | ### Code Climate local setup 3 | * You must download the following images one by one to your host system: 4 | * `docker pull codeclimate/codeclimate-structure` size `~ 1.9 Gb` of network traffic 5 | * `docker pull codeclimate/codeclimate-duplication` size `~ 100 - 200 Mb` of network traffic 6 | * `docker pull codeclimate/codeclimate` size `~ 100 - 200 Mb` of network traffic 7 | * **ATTENTION!!!** After uncompressing `codeclimate/codeclimate-structure` and `codeclimate/codeclimate-duplication` will use `10 Gb` of your disk space. 8 | * After that, you must open your `VSCode` in `Remote-Container` using command `Remote-Containers: Open Folder in Container` of commands explorer. 9 | * Inside VSCode terminal execute the following commands: 10 | * `npm run codeclimate:install` - this command checks presence of docker images and installs codeclimate wrapper to your VSCode docker container. 11 | 12 | ### Code Climate project analyze 13 | * For running codeclimate analysis, you must choose one of three report formats (`html`, `json`, `text`) and execute one of the following commands in the VSCode terminal: 14 | * `REPORT_FORMAT=html npm run codeclimate:analyze` - **html** fromat - a more comfortable way for visual analysis of results in browser 15 | * `REPORT_FORMAT=json npm run codeclimate:analyze` - **json** format - for analysis using any your tools 16 | * `REPORT_FORMAT=json npm run codeclimate:analyze` - **text** format - a simple way to view results without any special viewers 17 | * **IMPORTANT** - all reports will be stored in the `reports` folder, and each file has the following name format `%Y%m%d%H%M%S`.`(html|json|text)` 18 | 19 | ## `RU` 20 | ### Code Climate локальная установка 21 | * Вы должны скачать следующие образы, по одному, в вашу хост-систему (в порядке очередности): 22 | * `docker pull codeclimate/codeclimate-structure` размер `~ 1.9 Gb` сетевого трафика 23 | * `docker pull codeclimate/codeclimate-duplication` рвзмер `~ 100 - 200 Mb` сетевого трафика 24 | * `docker pull codeclimate/codeclimate` размер `~ 100 - 200 Mb` сетевого трафика 25 | * **ВНИМАНИЕ!!!** После распаковки `codeclimate/codeclimate-structure` и `codeclimate/codeclimate-duplication` будут вместе занимать `10 Gb` вашего дискового пространства. 26 | * Далее, нужно открыть текущий проект в `VSCode` и `Remote-Container` набрав команду `Remote-Containers: Open Folder in Container` в эксплорере команд. 27 | * Затем выполните команду: 28 | * `npm run codeclimate:install` - эта команда проверяет наличие необходимых образов в системе, затем устанавливает враппер `codeclimate`, в контейнер `VSCode`. 29 | 30 | ### Code Climate анализ проекта 31 | * Для запуска анализа кода через codeclimate, внутри контейнера VSCode, вам нужно определиться с одним из форматов отчета (`html`, `json`, `text`) и выполнить одну из следующих команд, в терминале VSCode: 32 | * `REPORT_FORMAT=html npm run codeclimate:analyze` - **html** формат - наиболее кофортный для визуального анализа через браузер 33 | * `REPORT_FORMAT=json npm run codeclimate:analyze` - **json** формат - удобен для использования результатов вашими инструментами, которые делают выводы о качестве кода. 34 | * `REPORT_FORMAT=json npm run codeclimate:analyze` - **text** формат - простейший вид отчета, который можно просмотреть на сервере и любом текстовом редакторе на клиенте. 35 | * **ВАЖНО** - все отчеты складируются в папке `reports` в корне проекта и в качестве имени указана текущая дата в формате `%Y%m%d%H%M%S`.`(html|json|text)` -------------------------------------------------------------------------------- /docs/DOCKER.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/docs/DOCKER.md -------------------------------------------------------------------------------- /docs/FOR_DEVELOPERS_EN.md: -------------------------------------------------------------------------------- 1 | # The purpose of this project 2 | * Helps to test your bot or application without using a real `Slack` server. It is also useful for writing integration and `e2e` tests. 3 | 4 | ### How to setup a project locally for development (VSCode are used to develop the project.) 5 | * In order to set up the working environment you need: 6 | * `Docker` application for your OS, you can download here https://docs.docker.com/install/ 7 | * Clone this project to your favorite location using command (for example): 8 | * `git clone https://github.com/maddevsio/mad-fake-slack.git` 9 | * Next, you need to install `VSCode` 10 | * Here are 3 quick steps to get started doing Visual Studio Code Remote Development: 11 | * Install the [stable version of VSCode](https://code.visualstudio.com) 12 | * Get the [Remote Development Extension Pack](https://aka.ms/VSCodeRemoteExtensionPack), which installs support for WSL, SSH, and Containers and is the easiest way to get started. If you don't need them all, you can uninstall the individual extensions. 13 | * Read the [Docs](https://aka.ms/vscode-remote). Try the [Dev Container samples](https://github.com/search?q=org%3AMicrosoft+vscode-remote-try-&unscoped_q=vscode-remote-try-). 14 | * Next you need to open the project in `VSCode` 15 | * Watch [Remote-Containers](https://youtu.be/TVcoGLL6Smo) on YouTube 16 | * `Reopen in Container` popup message will appear. 17 | * `VSCode` restarts and begins to create and configure containers. (Check that Docker service has been started) 18 | * Then you can open VSCode terminal, which, if everything is successful, will be connnected to the docker container for development, with all necessary tools installed in it. You will see a greeting like `root@e6e415dc0d02: /workspace` or `#/`in the case of `/bin/sh`. 19 | * Now you can work on project.`VSCode` is connected to the environment in the docker. 20 | 21 | ### Proof of work 22 | * In the current terminal VSCode, execute the command `npm start` 23 | * After that go to the URL `http://localhost:9001` in the browser (The current simple Fake Slak interface will be displayed) 24 | * To demonstrate, you need to open another remote terminal VSCode and execute the command `npm run example:rtmbot` (This command will run simple client bot application) 25 | * Then you will see in `UI` at the address `http://localhost:9001` a message from the bot. If you type a message and send it to the bot (using Enter or the "Send" button, you will receive an answer, with your message back). 26 | 27 | ### How to run example of integration tests between bot (from examples folder) and mad-fake-slack 28 | * For executing integration tests was used `jest-puppeeter`. 29 | * `jest` used as platform for writing tests in consolidation with power of `puppeeter` 30 | * You need to run command inside VSCode console `npm run example:rtmbot:integration` 31 | 32 | ### Testing api 33 | * Now supported following test api methods: 34 | * `GET` `/api/db/reset` - resets current db (If you will refresh mad-fake-slack ui in your browser, you will see that your conversational data has disappeared) 35 | * `GET` `/api/db/current/team?domain=NEW_DOMAIN_VALUE` - Sets the domain value for the current team (by default, value for `team.domain` equal to `localhost:9001`). Affects the domain for the URL when constructing a websocket url and workspace url. 36 | 37 | ### [List of NPM commands](NPMCOMMANDS.md) 38 | ### [Local setup and running code analysis using Code Climate](CODECLIMATE.md) -------------------------------------------------------------------------------- /docs/FOR_DEVELOPERS_RU.md: -------------------------------------------------------------------------------- 1 | # Цель данного проекта 2 | * Помочь в тестировании вашего бота или приложения, без использования реального сервера `Slack`. Пригодится так же для написания интеграционных и `e2e` тестов. 3 | 4 | ### Как поднять проект локально для разработки (Разработка ведется в VSCode) 5 | * Для того чтобы поднять рабочее окружение тебе понадобится: 6 | * `Docker` приложение для твоей ОС скачать можно тут https://docs.docker.com/install/ 7 | * Склонируйте текущий проект в любое удобное для вас расположение (используя к примеру, слудующую команду): 8 | * `git clone https://github.com/maddevsio/mad-fake-slack.git` 9 | * Далее необходимо установить `VSCode` 10 | * Вот 3 быстрых шага, чтобы начать делать удаленную разработку кода Visual Studio 11 | * Установите [стабильную сборку VSCode](https://code.visualstudio.com). 12 | * Получите [пакет расширений для удаленной разработки](https://aka.ms/VSCodeRemoteExtensionPack) 13 | * Прочитайте [документы](https://aka.ms/vscode-remote). Попробуйте [образцы Dev Container](https://github.com/search?q=org%3AMicrosoft+vscode-remote-try-&unscoped_q=vscode-remote-try-). 14 | * Далее нужно открыть проект в `VSCode`. 15 | * Посмотрите [видео по Remote-Containers](https://youtu.be/TVcoGLL6Smo) на Ютубе. 16 | * Появится всплывающее сообщение `Reopen in Container`. 17 | * VSCode перезагрузится и начнет поднимать контейнеры (Убедитесь что сервис докера запущен). 18 | * Далее можно вызвать терминал `VSCode`, который если все будет успешно, будет привязан к докеру для разработки, c установленными в нем всем необходимым. Вы увидите приветсвие типа `root@e6e415dc0d02:/workspace` или `#/`в случае `/bin/sh`. 19 | * Теперь можно разрабатывать, рабочее окружение поднято в докере и VSCode подключен к окружению в докере. 20 | 21 | ### Демонстрация работы 22 | * Далее в текущем терминале `VSCode`, выполнить команду `npm start` 23 | * И в браузере перейти по URL `http://localhost:9001` (Будет отображен текущий простой интерфейс Фейк-Слака) 24 | * Для демонстрации, нужно открыть еще один удаленный терминал `VSCode` и выполнить команду `npm run example:rtmbot` 25 | * Далее вы увидите в `UI` по адресу `http://localhost:9001` сообщение от бота. Если вы наберете сообщение и отправите его боту (используя Enter или кнопку "Отправить", то получите ответ, с вашим сообщением обратно). 26 | 27 | ### Как запустить пример интеграционных тестов между ботом (из папки examples) и mad-fake-slack 28 | * Для выполнения интеграционных тестов был использован `jest-puppeteer`. 29 | * `jest` используется в качестве платформы для написания тестов в консолидации с силой `puppeeter` 30 | * Вам необходимо запустить следующую команду в консоле VSCode `npm run example:rtmbot:integration` 31 | 32 | ### Тестовый api 33 | * Сейчас поддерживаются следующие тестовые апи: 34 | * `GET` `/api/db/reset` - сбрасывает текущую базу данных (если вы обновите пользовательский интерфейс mad-fake-slack в своем браузере, вы увидите, что ваши данные разговора исчезли) 35 | * `GET` `/api/db/current/team?domain=НОВОЕ_ДОМЕННОЕ_ИМЯ` - Усанвливает новое значение для домена текущей команды (по умолчанию, значение для `team.domain` равно `localhost:9001`). Влияет на домен для URL при выдаче веб-сокет урла и при отдаче урла workspace. 36 | 37 | ### [Список команд NPM](NPMCOMMANDS.md) 38 | ### [Локальная установка и запуск анализа кода при помощи Code Climate](CODECLIMATE.md) -------------------------------------------------------------------------------- /docs/NPMCOMMANDS.md: -------------------------------------------------------------------------------- 1 | ### [EN] List of npm commands 2 | ### [RU] Список команд npm 3 | 4 | ``` 5 | Lifecycle scripts included in mad-fake-slack: 6 | test 7 | npm run test:bdd 8 | start 9 | node index.js 10 | pretest 11 | eslint --ignore-path .gitignore . 12 | 13 | available via `npm run-script`: 14 | test:jest 15 | jest 16 | test:bdd 17 | npm run pretest && npx cucumber-js --fail-fast 18 | test:bdd:only 19 | npm run pretest && npx cucumber-js --fail-fast --tags=@only 20 | example:rtmbot 21 | SLACK_API=http://localhost:9001/api/ node examples/rtmbot/index.js 22 | example:rtmbot:bdd 23 | npm run pretest && SLACK_API=http://0.0.0.0:9001/api/ npm run test:jest -- --runInBand examples/rtmbot/ 24 | lint:hbs 25 | ember-template-lint views/* views/**/* 26 | codeclimate:install 27 | node scripts/codeclimate/check.js && sh scripts/codeclimate/setup.sh 28 | codeclimate:analyze:format:html 29 | node scripts/codeclimate/check.js && CODECLIMATE_CODE=$(node scripts/volumes/inspect.js) codeclimate analyze -f html > reports/$(date +"%Y%m%d%H%M%S").html 30 | codeclimate:analyze:format:json 31 | node scripts/codeclimate/check.js && CODECLIMATE_CODE=$(node scripts/volumes/inspect.js) codeclimate analyze -f json > reports/$(date +"%Y%m%d%H%M%S").json 32 | codeclimate:analyze:format:text 33 | node scripts/codeclimate/check.js && CODECLIMATE_CODE=$(node scripts/volumes/inspect.js) codeclimate analyze -f text > reports/$(date +"%Y%m%d%H%M%S").txt 34 | codeclimate:analyze 35 | npm run codeclimate:analyze:format:$REPORT_FORMAT 36 | ``` 37 | * `test:bdd` - 38 | * [EN] Runs bdd test for mad-fake-slack UI 39 | * [RU] Запускает интеграционные тесты для mad-fake-slack UI) 40 | * `test:bdd:only` 41 | * [EN] As a command above, but runs scripts with a `@only` marker (useful for running a single scenario without specifying long paths) 42 | * [RU] Как и предыдущая команда, только запускает сценарии помеченные марером `@only` (Очень полезно, если нужно запустить один или несколько разных сценариев, без необходимости указания длинных путей). 43 | * `example:rtmbot` - 44 | * [EN] Runs example of rtm bot 45 | * [RU] Запускает пример rtm бота 46 | * `example:rtmbot:bdd` 47 | * [EN] Runs bdd tests for rtm bot from `example` folder 48 | * [RU] Запускает интеграционные тесты для бота из папки `examples` 49 | * `lint:hbs` 50 | * [EN] Runs linter for hbs templates 51 | * [RU] Запускает линтинг для hbs шаблонов 52 | * `codeclimate:install` 53 | * [EN] Installs codeclimate wrapper inside VSCode container 54 | * [RU] Устанавливает обертку codeclimate в VSCode контейнер 55 | * `codeclimate:analyze:format:html` 56 | * [EN] Generate codeclimate report in html format after code analysis 57 | * [RU] Генерирует отчет в формате html после анализа кода 58 | * `codeclimate:analyze:format:json` 59 | * [EN] Generate codeclimate report in json format after code analysis 60 | * [RU] Генерирует отчет в формате json после анализа кода 61 | * `codeclimate:analyze:format:text` 62 | * [EN] Generate codeclimate report in text format after code analysis 63 | * [RU] Генерирует отчет в формате text после анализа кода 64 | * `codeclimate:analyze` 65 | * [EN] General command to run code analysis with the ability to set the report format using `REPORT_FORMAT` env variable 66 | * [RU] Общая команда, позволяющая запустить анализ кода с возможностью указать формат отчета через переменную окружения ]`REPORT_FORMAT` 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/ROADMAP.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/docs/ROADMAP.md -------------------------------------------------------------------------------- /docs/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/docs/images/demo.gif -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/docs/images/logo.png -------------------------------------------------------------------------------- /examples/rtmbot/__tests__/integration/actions.js: -------------------------------------------------------------------------------- 1 | async function getMessages(page) { 2 | return page.$$eval('span.c-message__body', 3 | spans => Array.from(spans).map(el => el.textContent.trim())); 4 | } 5 | 6 | module.exports = { 7 | getMessages 8 | }; 9 | -------------------------------------------------------------------------------- /examples/rtmbot/__tests__/integration/channel.test.js: -------------------------------------------------------------------------------- 1 | const { waitMs } = require('./utils'); 2 | const shared = require('./shared'); 3 | const actions = require('./actions'); 4 | 5 | describe('Channel communication', () => { 6 | let bot = null; 7 | 8 | beforeEach(async () => { 9 | bot = await shared.setup(); 10 | }); 11 | 12 | describe('general channel', () => { 13 | describe('when user sent message', () => { 14 | it('bot should display echo message', async () => { 15 | await page.keyboard.type('Hello'); 16 | await page.keyboard.press('Enter'); 17 | await waitMs(500); 18 | await expect(page).toMatchElement('span.c-message__body', 19 | { text: 'You sent text to the general channel: Hello' }); 20 | }); 21 | }); 22 | }); 23 | 24 | describe('random channel', () => { 25 | describe('when user sent message', () => { 26 | it('bot should display echo message', async () => { 27 | await await Promise.all([ 28 | page.waitForNavigation({ waitUntil: 'networkidle0' }), 29 | expect(page).toClick('span.p-channel_sidebar__name', { text: 'random' }) 30 | ]); 31 | await page.keyboard.type('Hello from random!'); 32 | await page.keyboard.press('Enter'); 33 | await waitMs(500); 34 | await expect(page).toMatchElement('span.c-message__body', 35 | { text: 'You sent text to the random channel: Hello from random!' }); 36 | const messages = await actions.getMessages(page); 37 | expect(messages).toHaveLength(6); 38 | expect(messages[messages.length - 1].trim()).toEqual('You sent text to the random channel: Hello from random!'); 39 | }); 40 | }); 41 | }); 42 | 43 | afterEach(async () => { 44 | await shared.teardown(bot); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /examples/rtmbot/__tests__/integration/direct.test.js: -------------------------------------------------------------------------------- 1 | const shared = require('./shared'); 2 | const actions = require('./actions'); 3 | 4 | describe('Direct channel communication', () => { 5 | let bot = null; 6 | 7 | beforeEach(async () => { 8 | bot = await shared.setup(); 9 | }); 10 | 11 | describe('bot direct channel', () => { 12 | describe('when user sent message', () => { 13 | it('bot should display echo message', async () => { 14 | await await Promise.all([ 15 | page.waitForNavigation({ waitUntil: 'networkidle0' }), 16 | expect(page).toClick('span.p-channel_sidebar__name', { text: /^Valera$/ }) 17 | ]); 18 | await page.keyboard.type('Hello from direct!'); 19 | await page.keyboard.press('Enter'); 20 | await expect(page).toMatchElement('span.c-message__body', 21 | { text: 'You sent text to me (direct): Hello from direct!' }); 22 | const messages = await actions.getMessages(page); 23 | expect(messages).toHaveLength(2); 24 | expect(messages[0].trim()).toEqual('Hello from direct!'); 25 | expect(messages[1].trim()).toEqual('You sent text to me (direct): Hello from direct!'); 26 | }); 27 | }); 28 | }); 29 | 30 | afterEach(async () => { 31 | await shared.teardown(bot); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/rtmbot/__tests__/integration/greeting.test.js: -------------------------------------------------------------------------------- 1 | const shared = require('./shared'); 2 | const actions = require('./actions'); 3 | 4 | describe("Bot's greeting", () => { 5 | let bot = null; 6 | 7 | beforeEach(async () => { 8 | bot = await shared.setup(); 9 | }); 10 | 11 | describe('when starts', () => { 12 | it('should display greeting', async () => { 13 | await expect(page).toMatchElement('span.c-message__body', { text: 'Hello there! I am a Valera!' }); 14 | const messages = await actions.getMessages(page); 15 | expect(messages).toHaveLength(1); 16 | }); 17 | }); 18 | 19 | afterEach(async () => { 20 | await shared.teardown(bot); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/rtmbot/__tests__/integration/shared.js: -------------------------------------------------------------------------------- 1 | 2 | const { startBot, waitMs, resetDb } = require('./utils'); 3 | 4 | async function setup() { 5 | await resetDb(); 6 | await page.goto('http://0.0.0.0:9001'); 7 | return startBot(); 8 | } 9 | 10 | async function teardown(bot) { 11 | if (bot) { 12 | await bot.destroy(); 13 | await waitMs(1000); 14 | await resetDb(); 15 | } 16 | } 17 | 18 | module.exports = { 19 | setup, 20 | teardown 21 | }; 22 | -------------------------------------------------------------------------------- /examples/rtmbot/__tests__/integration/utils.js: -------------------------------------------------------------------------------- 1 | const spawnd = require('spawnd'); 2 | const fetch = require('node-fetch'); 3 | const Bluebird = require('bluebird'); 4 | fetch.Promise = Bluebird; 5 | 6 | const SLACK_URL = 'http://0.0.0.0:9001'; 7 | 8 | async function waitMs(milliseconds = 0) { 9 | await new Promise((resolve) => setTimeout(resolve, milliseconds)); 10 | } 11 | 12 | async function startBot() { 13 | const proc = spawnd('npm run example:rtmbot', { shell: true }); 14 | await waitMs(2000); // Wait until bot starts 15 | return proc; 16 | } 17 | 18 | async function resetDb() { 19 | return fetch(`${SLACK_URL}/api/db/reset`); 20 | } 21 | 22 | module.exports = { 23 | startBot, 24 | waitMs, 25 | resetDb 26 | }; 27 | -------------------------------------------------------------------------------- /examples/rtmbot/index.js: -------------------------------------------------------------------------------- 1 | const { WebClient, LogLevel } = require('@slack/web-api'); 2 | const { RTMClient } = require('@slack/rtm-api'); 3 | 4 | const BOT_TOKEN = 'xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT'; 5 | const SLACK_API = process.env.SLACK_API || 'http://localhost:9001/api/'; 6 | 7 | const web = new WebClient(BOT_TOKEN, { slackApiUrl: SLACK_API, logLevel: LogLevel.DEBUG }); 8 | const rtm = new RTMClient(BOT_TOKEN, { slackApiUrl: SLACK_API, logLevel: LogLevel.DEBUG }); 9 | 10 | // Calling `rtm.on(eventName, eventHandler)` allows you to handle events (see: https://api.slack.com/events) 11 | // When the connection is active, the 'ready' event will be triggered 12 | let botUserId = null; 13 | let listOfChannels = []; 14 | 15 | rtm.on('ready', async () => { 16 | const { user_id: userId, user: userName } = await web.auth.test(); 17 | botUserId = userId; 18 | // eslint-disable-next-line no-console 19 | console.warn('[i] bot userId:', userId, ' and user name ', userName); 20 | listOfChannels = await web.channels.list({ exclude_archived: 1 }); 21 | const { id } = listOfChannels.channels.filter(({ name }) => name === 'general')[0]; 22 | // eslint-disable-next-line no-console 23 | console.warn('Channnel ID: ', id); 24 | const res = await rtm.sendMessage('`Hello there!` _I_ ~am~ a *Valera*!', id); 25 | // eslint-disable-next-line no-console 26 | console.warn('[i] Message sent: ', res.ts); 27 | }); 28 | 29 | rtm.on('message', async (event) => { 30 | const channels = listOfChannels.channels.filter(({ id }) => id === event.channel); 31 | const res = await rtm.sendMessage( 32 | `*You sent text to* ${botUserId === event.channel 33 | ? '`me` (direct)' 34 | : `the \`${channels[0].name}\` _channel_`}:\r\n ${event.text}`, event.channel 35 | ); 36 | // eslint-disable-next-line no-console 37 | console.warn('[i] Message sent: ', res.ts); 38 | }); 39 | 40 | // After the connection is open, your app will start receiving other events. 41 | rtm.on('user_typing', (event) => { 42 | // eslint-disable-next-line no-console 43 | console.log(event); 44 | }); 45 | 46 | rtm.start() 47 | // eslint-disable-next-line no-console 48 | .catch(console.error); 49 | -------------------------------------------------------------------------------- /features/auth_test_api.feature: -------------------------------------------------------------------------------- 1 | Feature: Http API request/response 2 | As a user, I want to make requests and 3 | receive responses from the JSON API 4 | 5 | Background: 6 | Given My timezone is "Asia/Bishkek" 7 | And Restart the api server with the following envs: 8 | | PORT | 3000 | 9 | 10 | Scenario: Make auth.test POST request with token in body 11 | Then I send "GET" request to "http://localhost:3000/api/db/current/team?domain=localhost:3000" with conditions 12 | """ 13 | { 14 | "request": { 15 | "headers": { 16 | "Content-Type": "application/json", 17 | "Authorization": "xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT" 18 | } 19 | }, 20 | "response": { 21 | "ok": true 22 | } 23 | } 24 | """ 25 | When I send "POST" request to "http://localhost:3000/api/auth.test" with conditions 26 | """ 27 | { 28 | "request": { 29 | "headers": { 30 | "Content-Type": "application/json" 31 | }, 32 | "body": { 33 | "token": "xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT" 34 | } 35 | }, 36 | "response": { 37 | "ok": true, 38 | "url": "http://localhost:3000/", 39 | "team": "BotFactory", 40 | "user": "valera", 41 | "team_id": "T12345678", 42 | "user_id": "W12345679" 43 | } 44 | } 45 | """ 46 | And Restart the api server with the following envs: 47 | | PORT | 9001 | 48 | 49 | Scenario: Make auth.test POST request with token in auth header 50 | Then I send "GET" request to "http://localhost:3000/api/db/current/team?domain=localhost:3000" with conditions 51 | """ 52 | { 53 | "request": { 54 | "headers": { 55 | "Content-Type": "application/json", 56 | "Authorization": "xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT" 57 | } 58 | }, 59 | "response": { 60 | "ok": true 61 | } 62 | } 63 | """ 64 | When I send "POST" request to "http://localhost:3000/api/auth.test" with conditions 65 | """ 66 | { 67 | "request": { 68 | "headers": { 69 | "Content-Type": "application/json", 70 | "Authorization": "xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT" 71 | } 72 | }, 73 | "response": { 74 | "ok": true, 75 | "url": "http://localhost:3000/", 76 | "team": "BotFactory", 77 | "user": "valera", 78 | "team_id": "T12345678", 79 | "user_id": "W12345679" 80 | } 81 | } 82 | """ 83 | And Restart the api server with the following envs: 84 | | PORT | 9001 | 85 | 86 | Scenario: Make auth.test POST request without token 87 | When I send "POST" request to "http://localhost:3000/api/auth.test" with conditions 88 | """ 89 | { 90 | "request": { 91 | "headers": { 92 | "Content-Type": "application/json" 93 | } 94 | }, 95 | "response": { 96 | "ok": false, 97 | "error": "invalid_auth" 98 | } 99 | } 100 | """ 101 | And Restart the api server with the following envs: 102 | | PORT | 9001 | 103 | 104 | Scenario: Set URL_SCHEMA to "https" and make auth.test POST request 105 | And Restart the api server with the following envs: 106 | | URL_SCHEMA | https | 107 | When I send "GET" request to "http://localhost:3000/api/db/current/team?domain=localhost:3000" with conditions 108 | """ 109 | { 110 | "request": { 111 | "headers": { 112 | "Content-Type": "application/json", 113 | "Authorization": "xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT" 114 | } 115 | }, 116 | "response": { 117 | "ok": true 118 | } 119 | } 120 | """ 121 | Then I send "POST" request to "http://localhost:3000/api/auth.test" with conditions 122 | """ 123 | { 124 | "request": { 125 | "headers": { 126 | "Content-Type": "application/json" 127 | }, 128 | "body": { 129 | "token": "xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT" 130 | } 131 | }, 132 | "response": { 133 | "ok": true, 134 | "url": "https://localhost:3000/", 135 | "team": "BotFactory", 136 | "user": "valera", 137 | "team_id": "T12345678", 138 | "user_id": "W12345679" 139 | } 140 | } 141 | """ 142 | And Restart the api server with the following envs: 143 | | PORT | 9001 | 144 | | URL_SCHEMA | | -------------------------------------------------------------------------------- /features/bold_message_formatting.feature: -------------------------------------------------------------------------------- 1 | Feature: Bold message formatting 2 | As a user, I want to send some words or all message in bold 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Only one word in bold 10 | And I type "*bold*" 11 | When I press the "Enter" keyboard button 12 | Then I should see "bold" in "Message body" 13 | And Message has the following HTML content at "last" position in "Message body": 14 | | html content | 15 | | bold | 16 | 17 | Scenario: Two words in bold sequentially 18 | And I type "*bold1* *bold2*" 19 | When I press the "Enter" keyboard button 20 | Then I should see "bold1 bold2" in "Message body" 21 | And Message has the following HTML content at "last" position in "Message body": 22 | | html content | 23 | | bold1 bold2 | 24 | 25 | Scenario: Word surrounded with stars which between 2 stars 26 | And I type "* *bold* *" 27 | When I press the "Enter" keyboard button 28 | Then I should see "* bold *" in "Message body" 29 | And Message has the following HTML content at "last" position in "Message body": 30 | | html content | 31 | | * bold * | 32 | 33 | Scenario: Formatting with preserving spaces at the beginning 34 | And I type "* bold*" 35 | When I press the "Enter" keyboard button 36 | Then I should see "bold" in "Message body" 37 | And Message has the following HTML content at "last" position in "Message body": 38 | | html content | 39 | |        bold | 40 | 41 | Scenario: Formatting with more than one word 42 | And I type "* bold line with spaces in it*" 43 | When I press the "Enter" keyboard button 44 | Then I should see "bold line with spaces in it" in "Message body" 45 | And Message has the following HTML content at "last" position in "Message body": 46 | | html content | 47 | |   bold line  with spaces   in    it | 48 | 49 | Scenario: Bold formatting of two words on different lines 50 | And I type "*first*" 51 | When I press the "Shift + Enter" keyboard button 52 | And I type "*second*" 53 | When I press the "Enter" keyboard button 54 | Then I should see "last" multiline message with: 55 | | Message body | first\nsecond | 56 | And Message has the following HTML content at "last" position in "Message body": 57 | | html content | 58 | | first
second | 59 | 60 | Scenario: Skip bold formatting if starts from double star 61 | And I type "**first*" 62 | When I press the "Enter" keyboard button 63 | Then I should see "last" multiline message with: 64 | | Message body | **first* | 65 | And Message has the following HTML content at "last" position in "Message body": 66 | | html content | 67 | | **first* | 68 | 69 | Scenario: Skip bold formatting if starts from double stars and spaces 70 | And I type "** first*" 71 | When I press the "Enter" keyboard button 72 | Then I should see "last" multiline message with: 73 | | Message body | ** first* | 74 | And Message has the following HTML content at "last" position in "Message body": 75 | | html content | 76 | | **   first* | 77 | 78 | Scenario: No bold formatting for word with space before ending star 79 | And I type "* bold *" 80 | When I press the "Enter" keyboard button 81 | Then I should see "* bold *" in "Message body" 82 | And Message has the following HTML content at "last" position in "Message body": 83 | | html content | 84 | | *       bold * | 85 | 86 | Scenario: Only many stars 87 | And I type "****************" 88 | When I press the "Enter" keyboard button 89 | Then I should see "****************" in "Message body" 90 | And Message has the following HTML content at "last" position in "Message body": 91 | | html content | 92 | | **************** | 93 | 94 | Scenario: Only stars separated with spaces 95 | And I type "** * * * * * * * *" 96 | When I press the "Enter" keyboard button 97 | Then I should see "** * * * * * * * *" in "Message body" 98 | And Message has the following HTML content at "last" position in "Message body": 99 | | html content | 100 | | ** * * *  * *   * *     * | 101 | 102 | Scenario: Word with a star at the beginning without a closing star 103 | And I type "*bold" 104 | When I press the "Enter" keyboard button 105 | Then I should see "*bold" in "Message body" 106 | And Message has the following HTML content at "last" position in "Message body": 107 | | html content | 108 | | *bold | 109 | 110 | Scenario: Only 2 stars 111 | And I type "**" 112 | When I press the "Enter" keyboard button 113 | Then I should see "**" in "Message body" 114 | And Message has the following HTML content at "last" position in "Message body": 115 | | html content | 116 | | ** | 117 | 118 | Scenario: Only 2 stars with any spaces between 119 | And I type "* *" 120 | When I press the "Enter" keyboard button 121 | Then I should see "* *" in "Message body" 122 | And Message has the following HTML content at "last" position in "Message body": 123 | | html content | 124 | | *     * | 125 | 126 | Scenario: Word with a double star at the beginning and at the end 127 | And I type "**bold**" 128 | When I press the "Enter" keyboard button 129 | Then I should see "**bold**" in "Message body" 130 | And Message has the following HTML content at "last" position in "Message body": 131 | | html content | 132 | | **bold** | 133 | 134 | Scenario: Word with a double star at the end 135 | And I type "*bold**" 136 | When I press the "Enter" keyboard button 137 | Then I should see "*bold**" in "Message body" 138 | And Message has the following HTML content at "last" position in "Message body": 139 | | html content | 140 | | *bold** | 141 | 142 | Scenario: With breakline separator 143 | And I type "*first line bold" 144 | When I press the "Shift + Enter" keyboard button 145 | And I type "second line bold*" 146 | When I press the "Enter" keyboard button 147 | Then I should see "last" multiline message with: 148 | | Message sender | Valera Petrov | 149 | | Message body | *first line bold\nsecond line bold* | 150 | And Message has the following HTML content at "last" position in "Message body": 151 | | html content | 152 | | *first line bold
second line bold* | -------------------------------------------------------------------------------- /features/breaks_at_the_any_position_of_message.feature: -------------------------------------------------------------------------------- 1 | Feature: Breaks in the middle of message 2 | As a user, I want to be able to insert breaks in my typed message 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Insert break in the middle of typed message 10 | And I type "first line second line" 11 | And I press the "ArrowLeft" keyboard button "12" times 12 | And I press the "Shift + Enter" keyboard button 13 | And I press the "Enter" keyboard button 14 | Then I should see "last" multiline message with: 15 | | Message body | first line\nsecond line | 16 | And Message has the following HTML content at "last" position in "Message body": 17 | | html content | 18 | | first line
second line | 19 | 20 | Scenario: Insert a break at the closest starting position of the typed message 21 | And I type "first line second line" 22 | And I set cursor to the "1" position of the text 23 | And I press the "Shift + Enter" keyboard button 24 | And I press the "Enter" keyboard button 25 | Then I should see "last" multiline message with: 26 | | Message body | f\nirst line second line | 27 | And Message has the following HTML content at "last" position in "Message body": 28 | | html content | 29 | | f
irst line second line | 30 | 31 | Scenario: Insert a break at the closest ending position of the typed message 32 | And I type "first line second line" 33 | And I set cursor to the "21" position of the text 34 | And I press the "Shift + Enter" keyboard button 35 | And I press the "Enter" keyboard button 36 | Then I should see "last" multiline message with: 37 | | Message body | first line second lin\ne | 38 | And Message has the following HTML content at "last" position in "Message body": 39 | | html content | 40 | | first line second lin
e | -------------------------------------------------------------------------------- /features/code_message_formatting.feature: -------------------------------------------------------------------------------- 1 | Feature: Code message formatting 2 | As a user, I want to send some words or all message in code 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Only one word in code 10 | And I type "`code`" 11 | When I press the "Enter" keyboard button 12 | Then I should see "code" in "Message body" 13 | And Message has the following HTML content at "last" position in "Message body": 14 | | html content | 15 | | code | 16 | 17 | Scenario: Two words in code 18 | And I type "`some code`" 19 | When I press the "Enter" keyboard button 20 | Then I should see "some code" in "Message body" 21 | And Message has the following HTML content at "last" position in "Message body": 22 | | html content | 23 | | some code | 24 | 25 | Scenario: Two code blocks one by one 26 | And I type "`code1` `code2`" 27 | When I press the "Enter" keyboard button 28 | Then I should see "code1 code2" in "Message body" 29 | And Message has the following HTML content at "last" position in "Message body": 30 | | html content | 31 | | code1 code2 | 32 | 33 | Scenario: Two code blocks on separate lines 34 | And I type "`code1`" 35 | When I press the "Shift + Enter" keyboard button 36 | And I type "`code2`" 37 | When I press the "Enter" keyboard button 38 | Then I should see "last" multiline message with: 39 | | Message body | code1\ncode2 | 40 | And Message has the following HTML content at "last" position in "Message body": 41 | | html content | 42 | | code1
code2 | 43 | 44 | Scenario: Three code blocks on separate lines 45 | And I type "`code1`" 46 | When I press the "Shift + Enter" keyboard button 47 | And I type "`code2`" 48 | When I press the "Shift + Enter" keyboard button 49 | And I type "`code3`" 50 | When I press the "Enter" keyboard button 51 | Then I should see "last" multiline message with: 52 | | Message body | code1\ncode2\ncode3 | 53 | And Message has the following HTML content at "last" position in "Message body": 54 | | html content | 55 | | code1
code2
code3 | 56 | 57 | Scenario: Three code blocks on separate lines with more than one breaklines 58 | And I type "`code1`" 59 | When I press the "Shift + Enter" keyboard button 60 | When I press the "Shift + Enter" keyboard button 61 | And I type "`code2`" 62 | When I press the "Shift + Enter" keyboard button 63 | When I press the "Shift + Enter" keyboard button 64 | And I type "`code3`" 65 | When I press the "Shift + Enter" keyboard button 66 | When I press the "Enter" keyboard button 67 | Then I should see "last" multiline message with: 68 | | Message body | code1\ncode2\ncode3 | 69 | And Message has the following HTML content at "last" position in "Message body": 70 | """ 71 | code1code2code3 72 | """ 73 | 74 | Scenario: Include any count of ` from the end 75 | And I type "`code1````" 76 | When I press the "Enter" keyboard button 77 | Then I should see "code1```" in "Message body" 78 | And Message has the following HTML content at "last" position in "Message body": 79 | | html content | 80 | | code1``` | 81 | 82 | Scenario: Preserve spaces 83 | And I type "` code `" 84 | When I press the "Enter" keyboard button 85 | Then I should see "code" in "Message body" 86 | And Message has the following HTML content at "last" position in "Message body": 87 | | html content | 88 | | code | 89 | 90 | Scenario: Format with excluding extra apostrophe at end 91 | And I type "`code``" 92 | When I press the "Enter" keyboard button 93 | Then I should see "code`" in "Message body" 94 | And Message has the following HTML content at "last" position in "Message body": 95 | | html content | 96 | | code` | 97 | 98 | Scenario: Format with triple apostrophe at end 99 | And I type "`code```" 100 | When I press the "Enter" keyboard button 101 | Then I should see "code``" in "Message body" 102 | And Message has the following HTML content at "last" position in "Message body": 103 | | html content | 104 | | code`` | 105 | 106 | Scenario: Ignore strike formatting symbol inside 107 | And I type "`~code~`" 108 | When I press the "Enter" keyboard button 109 | Then I should see "~code~" in "Message body" 110 | And Message has the following HTML content at "last" position in "Message body": 111 | | html content | 112 | | ~code~ | 113 | 114 | Scenario: Ignore italic formatting symbol inside 115 | And I type "`_code_`" 116 | When I press the "Enter" keyboard button 117 | Then I should see "_code_" in "Message body" 118 | And Message has the following HTML content at "last" position in "Message body": 119 | | html content | 120 | | _code_ | 121 | 122 | Scenario: Ignore bold formatting symbol inside 123 | And I type "`*code*`" 124 | When I press the "Enter" keyboard button 125 | Then I should see "*code*" in "Message body" 126 | And Message has the following HTML content at "last" position in "Message body": 127 | | html content | 128 | | *code* | 129 | 130 | Scenario: No formatting without end of code block 131 | And I type "`some code" 132 | When I press the "Enter" keyboard button 133 | Then I should see "`some code" in "Message body" 134 | And Message has the following HTML content at "last" position in "Message body": 135 | | html content | 136 | | `some code | 137 | 138 | Scenario: No formatting without begin of code block 139 | And I type "some code`" 140 | When I press the "Enter" keyboard button 141 | Then I should see "some code`" in "Message body" 142 | And Message has the following HTML content at "last" position in "Message body": 143 | | html content | 144 | | some code` | 145 | 146 | Scenario: No formatting for empty code block 147 | And I type "``" 148 | When I press the "Enter" keyboard button 149 | Then I should see "``" in "Message body" 150 | And Message has the following HTML content at "last" position in "Message body": 151 | | html content | 152 | | `` | 153 | 154 | Scenario: No formatting for code block with only spaces in it 155 | And I type "` `" 156 | When I press the "Enter" keyboard button 157 | Then I should see "` `" in "Message body" 158 | And Message has the following HTML content at "last" position in "Message body": 159 | | html content | 160 | | `      ` | 161 | 162 | Scenario: No formatting if additional apostrophe at the begin 163 | And I type "``code`" 164 | When I press the "Enter" keyboard button 165 | Then I should see "``code`" in "Message body" 166 | And Message has the following HTML content at "last" position in "Message body": 167 | | html content | 168 | | ``code` | 169 | 170 | Scenario: No formatting with breaklines inside code block 171 | And I type "`some" 172 | When I press the "Shift + Enter" keyboard button 173 | And I type "code`" 174 | When I press the "Enter" keyboard button 175 | Then I should see "last" multiline message with: 176 | | Message body | `some\ncode` | 177 | And Message has the following HTML content at "last" position in "Message body": 178 | | html content | 179 | | `some
code` | -------------------------------------------------------------------------------- /features/edit_last_message.feature: -------------------------------------------------------------------------------- 1 | Feature: 2 | As a user, I want to edit my last message 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | And User "Valera" connected to fake slack using parameters: 9 | | token | xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT | 10 | | url | http://localhost:9001/api/ | 11 | 12 | Scenario: Edit last sent message 13 | And I type "first message" 14 | And I press the "Enter" keyboard button 15 | And I should see "last" multiline "Message item" with: 16 | | Message sender | Valera Petrov | 17 | | Message body | first message | 18 | When I press the "ArrowUp" keyboard button 19 | And I type " edited" 20 | And I click on "button" with text "⏎ Save Changes" without navigation 21 | And I'm waiting for "Inline Message Editor" to be hidden 22 | Then I should see "last" multiline "Message item" with: 23 | | Message sender | Valera Petrov | 24 | | Message body | first message edited | 25 | 26 | Scenario: Edit my message, which is displayed before the bot message 27 | And I type "very first message" 28 | And I press the "Enter" keyboard button 29 | And I should see "last" multiline "Message item" with: 30 | | Message sender | Valera Petrov | 31 | | Message body | very first message | 32 | And User "Valera" send message: 33 | | type | message | 34 | | text | from Valera | 35 | | channel | general | 36 | When I press the "ArrowUp" keyboard button 37 | And I type " edited" 38 | And I click on "button" with text "⏎ Save Changes" without navigation 39 | And I'm waiting for "Inline Message Editor" to be hidden 40 | Then I should see "first" multiline "Message item" with: 41 | | Message sender | Valera Petrov | 42 | | Message body | very first message edited | 43 | 44 | Scenario: Edit my last message in sequence between bot and me 45 | And I send "first message" to chat 46 | And User "Valera" send message: 47 | | type | message | 48 | | text | from Valera | 49 | | channel | general | 50 | And I should see "last" multiline "Message item" with: 51 | | Message sender | Valera | 52 | | Message body | from Valera | 53 | And I send "second message" to chat 54 | When I press the "ArrowUp" keyboard button 55 | And I type " edited" 56 | And I click on "button" with text "⏎ Save Changes" without navigation 57 | And I'm waiting for "Inline Message Editor" to be hidden 58 | Then I should see "last" multiline "Message item" with: 59 | | Message sender | Valera Petrov | 60 | | Message body | second message edited | 61 | 62 | Scenario: Cancel editing my message 63 | And I send "first message" to chat 64 | And I press the "ArrowUp" keyboard button 65 | And I type " edited" 66 | When I click on "button" with text "Cancel" without navigation 67 | And I'm waiting for "Inline Message Editor" to be hidden 68 | Then I should see "last" multiline "Message item" with: 69 | | Message sender | Valera Petrov | 70 | | Message body | first message | 71 | 72 | Scenario: Cancel editing my message by pressing Escape button 73 | And I send "first message" to chat 74 | And I press the "ArrowUp" keyboard button 75 | And I type " edited" 76 | When I press the "Escape" keyboard button 77 | And I'm waiting for "Inline Message Editor" to be hidden 78 | Then I should see "last" multiline "Message item" with: 79 | | Message sender | Valera Petrov | 80 | | Message body | first message | 81 | 82 | Scenario: Changes in the message are persistent 83 | And I send "first message" to chat 84 | And I press the "ArrowUp" keyboard button 85 | And I type " edited" 86 | And I click on "button" with text "⏎ Save Changes" without navigation 87 | And I'm waiting for "Inline Message Editor" to be hidden 88 | When I reload the page 89 | Then I should see "last" multiline "Message item" with: 90 | | Message sender | Valera Petrov | 91 | | Message body | first message edited | 92 | 93 | Scenario: Don't show editor for last message when message textbox is not empty 94 | And I send "first message" to chat 95 | And I type multiline message: 96 | """ 97 | new first line 98 | and then second line 99 | and third line 100 | """ 101 | When I press the "ArrowUp" keyboard button 102 | Then I should not see "Inline Message Editor" 103 | 104 | Scenario: Should not block moving cursor to up 105 | And I send "first message" to chat 106 | And I type multiline message: 107 | """ 108 | new first line 109 | and then second line 110 | and third line 111 | """ 112 | And I memorize the "selectionStart" of "Input message" 113 | When I press the "ArrowUp" keyboard button 114 | And The "selectionStart" with type "Number" of the "Input message" must "toBeLessThan" last 115 | And I press the "ArrowUp" keyboard button 116 | Then The "selectionStart" with type "Number" of the "Input message" must "toBeLessThan" last 117 | 118 | Scenario: Don't show editor message for textbox which has space symbol in it 119 | And I send "first message" to chat 120 | And I type " " 121 | When I press the "ArrowUp" keyboard button 122 | Then I should not see "Inline Message Editor" 123 | 124 | Scenario: Don't show editor message for textbox which has any symbol in it 125 | And I send "first message" to chat 126 | And I type "a" 127 | When I press the "ArrowUp" keyboard button 128 | Then I should not see "Inline Message Editor" -------------------------------------------------------------------------------- /features/hide_message_header.feature: -------------------------------------------------------------------------------- 1 | Feature: Hide header for sequence of messages from one user 2 | As a user, I don't want to see header for each one by one messages from me 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | And User "Valera" connected to fake slack using parameters: 9 | | token | xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT | 10 | | url | http://localhost:9001/api/ | 11 | 12 | Scenario: Display header for first message from sequence 13 | And I type "first message" 14 | And I press the "Enter" keyboard button 15 | And I should see "last" multiline "Message item" with: 16 | | Message sender | Valera Petrov | 17 | | Message body | first message | 18 | When I type "second message" 19 | And I press the "Enter" keyboard button 20 | Then I should see "last" multiline "Message item" with: 21 | | Message sender | | 22 | | Message body | second message | 23 | 24 | Scenario: Display header for message sequence between bot and client one by one 25 | And I type "first message" 26 | And I press the "Enter" keyboard button 27 | And I should see "last" multiline "Message item" with: 28 | | Message sender | Valera Petrov | 29 | | Message body | first message | 30 | When User "Valera" send message: 31 | | type | message | 32 | | text | first bot message | 33 | | channel | general | 34 | Then I should see "last" multiline "Message item" with: 35 | | Message sender | Valera | 36 | | App badge | APP | 37 | | Message body | first bot message | 38 | 39 | Scenario: Display only header only for first message in sequence between bot and client 40 | And I send "first message" to chat 41 | And I should see "last" multiline "Message item" with: 42 | | Message sender | Valera Petrov | 43 | | Message body | first message | 44 | And I send "second message" to chat 45 | And I should see "last" multiline "Message item" with: 46 | | Message sender | | 47 | | Message body | second message | 48 | And User "Valera" send message: 49 | | type | message | 50 | | text | first bot message | 51 | | channel | general | 52 | And I should see "last" multiline "Message item" with: 53 | | Message sender | Valera | 54 | | App badge | APP | 55 | | Message body | first bot message | 56 | When User "Valera" send message: 57 | | type | message | 58 | | text | second bot message | 59 | | channel | general | 60 | Then I should see "last" multiline "Message item" with: 61 | | Message sender | | 62 | | App badge | | 63 | | Message body | second bot message | 64 | 65 | Scenario: Show message header if time interval passed 66 | And Now is the date and time "2019-09-04T06:50:53.953Z" 67 | And I send "first message" to chat 68 | And I should see "last" multiline "Message item" with: 69 | | Message sender | Valera Petrov | 70 | | Message body | first message | 71 | When Now "6" minutes passed 72 | And I send "second message" to chat 73 | Then I should see "last" multiline "Message item" with: 74 | | Message sender | Valera Petrov | 75 | | Message body | second message | 76 | 77 | Scenario: Show message header for message which break the sequence 78 | And Now is the date and time "2019-09-04T06:50:53.953Z" 79 | And I send "first message" to chat 80 | And I should see "last" multiline "Message item" with: 81 | | Message sender | Valera Petrov | 82 | | Message body | first message | 83 | And Now "3" minutes passed 84 | And User "Valera" send message: 85 | | type | message | 86 | | text | first bot message | 87 | | channel | general | 88 | And I should see "last" multiline "Message item" with: 89 | | Message sender | Valera | 90 | | Message body | first bot message | 91 | When Now "4" minutes passed 92 | And I send "second message" to chat 93 | Then I should see "last" multiline "Message item" with: 94 | | Message sender | Valera Petrov | 95 | | Message body | second message | 96 | 97 | Scenario: Hide message header after first bot message in sequence 98 | And Now is the date and time "2019-09-04T06:50:53.953Z" 99 | And User "Valera" send message: 100 | | type | message | 101 | | text | first bot message | 102 | | channel | general | 103 | And I should see "last" multiline "Message item" with: 104 | | Message sender | Valera | 105 | | Message body | first bot message | 106 | And Now "1" minutes passed 107 | When User "Valera" send message: 108 | | type | message | 109 | | text | second bot message | 110 | | channel | general | 111 | Then I should see "last" multiline "Message item" with: 112 | | Message sender | | 113 | | Message body | second bot message | 114 | 115 | Scenario: Show message header after time interval passed for next bot message 116 | And Now is the date and time "2019-09-04T06:50:53.953Z" for "Valera" user 117 | And User "Valera" send message: 118 | | type | message | 119 | | text | first bot message | 120 | | channel | general | 121 | And I should see "last" multiline "Message item" with: 122 | | Message sender | Valera | 123 | | Message body | first bot message | 124 | And Now "6" minutes passed for "Valera" user 125 | When User "Valera" send message: 126 | | type | message | 127 | | text | second bot message | 128 | | channel | general | 129 | Then I should see "last" multiline "Message item" with: 130 | | Message sender | Valera | 131 | | Message body | second bot message | -------------------------------------------------------------------------------- /features/incoming_message_payload.feature: -------------------------------------------------------------------------------- 1 | Feature: Incoming message payload 2 | As a user, I want to be sure that 3 | payload of incoming message is correct 4 | 5 | Background: 6 | Given My timezone is "Asia/Bishkek" 7 | And Fake slack db is empty 8 | And I am on "fake slack ui" page 9 | And User "Valera" connected to fake slack using parameters: 10 | | token | xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT | 11 | | url | http://localhost:9001/api/ | 12 | 13 | Scenario: Check incoming message payload from "general" channel 14 | And I type "general text message" 15 | When I press the "Enter" keyboard button 16 | Then I should see "general text message" in "Message body" 17 | And User "Valera" should receive "incoming" payload with "message" type: 18 | | field | type | required | format | 19 | | client_msg_id | string | true | uuid | 20 | | suppress_notification | boolean | true | | 21 | | type | string | true | slackmtypes | 22 | | text | string | true | | 23 | | user | string | true | slackuid | 24 | | team | string | true | slacktid | 25 | | user_team | string | true | slacktid | 26 | | source_team | string | true | slacktid | 27 | | channel | string | true | slackchid | 28 | | event_ts | string | true | slackts | 29 | | ts | string | true | slackts | 30 | 31 | Scenario: Check incoming message payload from "random" channel 32 | And I click on "channel item" with text "random" 33 | And I type "random text message" 34 | When I press the "Enter" keyboard button 35 | Then I should see "random text message" in "Message body" on the "last" position 36 | And User "Valera" should receive "incoming" payload with "message" type: 37 | | field | type | required | format | 38 | | client_msg_id | string | true | uuid | 39 | | suppress_notification | boolean | true | | 40 | | type | string | true | slackmtypes | 41 | | text | string | true | | 42 | | user | string | true | slackuid | 43 | | team | string | true | slacktid | 44 | | user_team | string | true | slacktid | 45 | | source_team | string | true | slacktid | 46 | | channel | string | true | slackchid | 47 | | event_ts | string | true | slackts | 48 | | ts | string | true | slackts | -------------------------------------------------------------------------------- /features/incoming_user_typing_payload.feature: -------------------------------------------------------------------------------- 1 | Feature: Incoming user typing message payload 2 | As a user, I want to broadcast event 3 | about my activity in the text box 4 | every 5 seconds 5 | 6 | Background: 7 | Given My timezone is "Asia/Bishkek" 8 | And Fake slack db is empty 9 | And I am on "fake slack ui" page 10 | And User "Valera" connected to fake slack using parameters: 11 | | token | xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT | 12 | | url | http://localhost:9001/api/ | 13 | 14 | Scenario: Send "user_typing" event to "general" channel 15 | And I type "general text message" 16 | Then User "Valera" should receive "incoming" payload with "user_typing" type: 17 | | field | type | required | format | 18 | | type | string | true | slackmtypes | 19 | | user | string | true | slackuid | 20 | | channel | string | true | slackchid | 21 | 22 | Scenario: Send "user_typing" event to "random" channel 23 | And I click on "channel item" with text "random" 24 | And I type "random text message" 25 | And User "Valera" should receive "incoming" payload with "user_typing" type: 26 | | field | type | required | format | 27 | | type | string | true | slackmtypes | 28 | | user | string | true | slackuid | 29 | | channel | string | true | slackchid | -------------------------------------------------------------------------------- /features/inline_message_editor_linebreaks.feature: -------------------------------------------------------------------------------- 1 | Feature: Breaks in the middle of inline edited message 2 | As a user, I want to be able to insert breaks for inline message editing 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Finish editing the last sent message by pressing Enter 10 | And I send "first message" to chat 11 | And I should see "last" multiline "Message item" with: 12 | | Message sender | Valera Petrov | 13 | | Message body | first message | 14 | When I press the "ArrowUp" keyboard button 15 | And I type " edited" 16 | And I press the "Enter" keyboard button 17 | And I'm waiting for "Inline Message Editor" to be hidden 18 | Then I should see "last" multiline "Message item" with: 19 | | Message sender | Valera Petrov | 20 | | Message body | first message edited | 21 | 22 | Scenario: Edit inline message with additional line 23 | And I send "first line" to chat 24 | And I should see "last" multiline "Message item" with: 25 | | Message sender | Valera Petrov | 26 | | Message body | first line | 27 | When I press the "ArrowUp" keyboard button 28 | And I press the "Shift + Enter" keyboard button 29 | And I type "second line" 30 | And I press the "Enter" keyboard button 31 | And I'm waiting for "Inline Message Editor" to be hidden 32 | Then I should see "last" multiline "Message item" with: 33 | | Message sender | Valera Petrov | 34 | | Message body | first line\nsecond line | 35 | 36 | Scenario: Add new line using Ctrl+Enter 37 | And I send "first line" to chat 38 | And I should see "last" multiline "Message item" with: 39 | | Message sender | Valera Petrov | 40 | | Message body | first line | 41 | When I press the "ArrowUp" keyboard button 42 | And I press the "Control + Enter" keyboard button 43 | And I type "second line" 44 | And I press the "Enter" keyboard button 45 | And I'm waiting for "Inline Message Editor" to be hidden 46 | Then I should see "last" multiline "Message item" with: 47 | | Message sender | Valera Petrov | 48 | | Message body | first line\nsecond line | 49 | 50 | Scenario: Insert break in the middle of typed message 51 | And I send "first line second line" to chat 52 | When I press the "ArrowUp" keyboard button 53 | And I press the "ArrowLeft" keyboard button "12" times 54 | And I press the "Shift + Enter" keyboard button 55 | And I press the "Enter" keyboard button 56 | And I'm waiting for "Inline Message Editor" to be hidden 57 | Then I should see "last" multiline "Message item" with: 58 | | Message sender | Valera Petrov | 59 | | Message body | first line\nsecond line | 60 | And Message has the following HTML content at "last" position in "Message body": 61 | | html content | 62 | | first line
second line | 63 | 64 | Scenario: Re-editing message with new line should not break previous breaklines 65 | And I type "first line" 66 | And I press the "Shift + Enter" keyboard button 67 | And I type "second line" 68 | And I press the "Enter" keyboard button 69 | When I press the "ArrowUp" keyboard button 70 | And I press the "Shift + Enter" keyboard button 71 | And I type "new line" 72 | And I press the "Enter" keyboard button 73 | And I'm waiting for "Inline Message Editor" to be hidden 74 | Then I should see "last" multiline "Message item" with: 75 | | Message sender | Valera Petrov | 76 | | Message body | first line\nsecond line\nnew line | 77 | And Message has the following HTML content at "last" position in "Message body": 78 | | html content | 79 | | first line
second line
new line | 80 | -------------------------------------------------------------------------------- /features/italic_message_formatting.feature: -------------------------------------------------------------------------------- 1 | Feature: Italic message formatting 2 | As a user, I want to send some words or all message in italic format 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Only one word in italic 10 | And I type "_italic_" 11 | When I press the "Enter" keyboard button 12 | Then I should see "italic" in "Message body" 13 | And Message has the following HTML content at "last" position in "Message body": 14 | | html content | 15 | | italic | 16 | 17 | Scenario: Only one word with any spaces in italic 18 | And I type "_ italic _" 19 | When I press the "Enter" keyboard button 20 | Then I should see "italic" in "Message body" 21 | And Message has the following HTML content at "last" position in "Message body": 22 | | html content | 23 | |    italic   | 24 | 25 | Scenario: Word surrounded with spaces and underscores 26 | And I type "_ _ italic _ _" 27 | When I press the "Enter" keyboard button 28 | Then I should see "_ italic _" in "Message body" 29 | And Message has the following HTML content at "last" position in "Message body": 30 | | html content | 31 | |    italic   _ | 32 | 33 | Scenario: Two italic blocks on different lines 34 | And I type "_one_" 35 | And I press the "Shift + Enter" keyboard button 36 | And I type "_two_" 37 | When I press the "Enter" keyboard button 38 | Then I should see "last" multiline message with: 39 | | Message body | one\ntwo | 40 | And Message has the following HTML content at "last" position in "Message body": 41 | | html content | 42 | | one
two | 43 | 44 | Scenario: Skip italic block with breakline inside 45 | And I type "_italic" 46 | And I press the "Shift + Enter" keyboard button 47 | And I type "_" 48 | When I press the "Enter" keyboard button 49 | Then I should see "last" multiline message with: 50 | | Message body | _italic\n_ | 51 | And Message has the following HTML content at "last" position in "Message body": 52 | | html content | 53 | | _italic
_ | 54 | 55 | Scenario: Skip italic block surrounded by double underscores 56 | And I type "__italic__" 57 | When I press the "Enter" keyboard button 58 | Then I should see "last" multiline message with: 59 | | Message body | __italic__ | 60 | And Message has the following HTML content at "last" position in "Message body": 61 | | html content | 62 | | __italic__ | 63 | 64 | Scenario: Skip italic block surrounded by double underscores and spaces 65 | And I type "__ italic __" 66 | When I press the "Enter" keyboard button 67 | Then I should see "last" multiline message with: 68 | | Message body | __ italic __ | 69 | And Message has the following HTML content at "last" position in "Message body": 70 | | html content | 71 | | __ italic __ | 72 | 73 | Scenario: Skip italic block with empty content 74 | And I type "__" 75 | When I press the "Enter" keyboard button 76 | Then I should see "last" multiline message with: 77 | | Message body | __ | 78 | And Message has the following HTML content at "last" position in "Message body": 79 | | html content | 80 | | __ | 81 | 82 | Scenario: Skip italic block with spaced content 83 | And I type "_ _" 84 | When I press the "Enter" keyboard button 85 | Then I should see "last" multiline message with: 86 | | Message body | _ _ | 87 | And Message has the following HTML content at "last" position in "Message body": 88 | | html content | 89 | | _     _ | 90 | 91 | Scenario: Skip italic block with underscore content 92 | And I type "___" 93 | When I press the "Enter" keyboard button 94 | Then I should see "last" multiline message with: 95 | | Message body | ___ | 96 | And Message has the following HTML content at "last" position in "Message body": 97 | | html content | 98 | | ___ | 99 | 100 | Scenario: Skip italic block with double underscore content 101 | And I type "____" 102 | When I press the "Enter" keyboard button 103 | Then I should see "last" multiline message with: 104 | | Message body | ____ | 105 | And Message has the following HTML content at "last" position in "Message body": 106 | | html content | 107 | | ____ | -------------------------------------------------------------------------------- /features/multiline_messages.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiline messages 2 | As a user, I want to send messages 3 | with my own linebreaks 4 | between sentences or parts of a message. 5 | 6 | Background: 7 | Given My timezone is "Asia/Bishkek" 8 | And Fake slack db is empty 9 | And I am on "fake slack ui" page 10 | 11 | Scenario: Add linebreaks using `Ctrl + Enter` 12 | And I type "first line 1" 13 | And I press the "Control + Enter" keyboard button 14 | And I type "second line 1" 15 | When I press the "Enter" keyboard button 16 | Then I should see "last" multiline message with: 17 | | Message sender | Valera Petrov | 18 | | Message body | first line 1\nsecond line 1 | 19 | 20 | Scenario: Removes linebreaks (`Ctrl + Enter`) at the begining of message 21 | And I press the "Control + Enter" keyboard button 22 | And I press the "Control + Enter" keyboard button 23 | And I press the "Control + Enter" keyboard button 24 | And I type "second line 1" 25 | When I press the "Enter" keyboard button 26 | Then I should see "last" multiline message with: 27 | | Message sender | Valera Petrov | 28 | | Message body | second line 1 | 29 | 30 | Scenario: Removes linebreaks (`Ctrl + Enter`) at the end of message 31 | And I type "first line 1" 32 | And I press the "Control + Enter" keyboard button 33 | And I press the "Control + Enter" keyboard button 34 | And I press the "Control + Enter" keyboard button 35 | When I press the "Enter" keyboard button 36 | Then I should see "last" multiline message with: 37 | | Message sender | Valera Petrov | 38 | | Message body | first line 1 | 39 | 40 | Scenario: Add linebreaks using `Shift + Enter` 41 | And I type "first line 2" 42 | And I press the "Shift + Enter" keyboard button 43 | And I type "second line 2" 44 | When I press the "Enter" keyboard button 45 | Then I should see "last" multiline message with: 46 | | Message sender | Valera Petrov | 47 | | Message body | first line 2\nsecond line 2 | 48 | 49 | Scenario: Removes linebreaks (`Shift + Enter`) at the begin of message 50 | And I press the "Shift + Enter" keyboard button 51 | And I press the "Shift + Enter" keyboard button 52 | And I press the "Shift + Enter" keyboard button 53 | And I type "second line 2" 54 | When I press the "Enter" keyboard button 55 | Then I should see "last" multiline message with: 56 | | Message sender | Valera Petrov | 57 | | Message body | second line 2 | 58 | 59 | Scenario: Removes linebreaks (`Shift + Enter`) at the end of message 60 | And I type "first line 2" 61 | And I press the "Shift + Enter" keyboard button 62 | And I press the "Shift + Enter" keyboard button 63 | And I press the "Shift + Enter" keyboard button 64 | When I press the "Enter" keyboard button 65 | Then I should see "last" multiline message with: 66 | | Message sender | Valera Petrov | 67 | | Message body | first line 2 | 68 | 69 | Scenario: Paste multiline text from clipboard to messagebox 70 | And I copied the following text to the clipboard: 71 | | text | 72 | | first\nsecond\nthird | 73 | And I memorize the "clientHeight" of "Input message" 74 | When I press the "Control + KeyV" keyboard button 75 | And The "clientHeight" with type "Number" of the "Input message" must "toBeGreaterThan" last 76 | 77 | Scenario: The maximum height growth for the contents of a multi-line message 78 | And I memorize the "clientHeight" of "Input message" 79 | And I type "line 1" 80 | And I press the "Control + Enter" keyboard button 81 | And The "clientHeight" with type "Number" of the "Input message" must "toBeGreaterThan" last 82 | And I type "line 2" 83 | And I press the "Control + Enter" keyboard button 84 | And I type "line 3" 85 | And I press the "Control + Enter" keyboard button 86 | And I type "line 4" 87 | And I press the "Control + Enter" keyboard button 88 | And I type "line 5" 89 | And I press the "Control + Enter" keyboard button 90 | And I memorize the "clientHeight" of "Input message" 91 | And I type "line 6" 92 | And I press the "Control + Enter" keyboard button 93 | And The "clientHeight" with type "Number" of the "Input message" must "toBeGreaterThan" last 94 | And I type "line 7" 95 | When I memorize the "clientHeight" of "Input message" 96 | And I press the "Control + Enter" keyboard button 97 | And I type "line 8" 98 | And I press the "Control + Enter" keyboard button 99 | And I type "line 9" 100 | Then The "clientHeight" with type "Number" of the "Input message" must "toEqual" last -------------------------------------------------------------------------------- /features/pagecontent.feature: -------------------------------------------------------------------------------- 1 | Feature: Content of fake slack main page 2 | As a user I want to see content on main page 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Team name content 10 | Then I should see "BotFactory" in "Team name" 11 | 12 | Scenario: Team user name content 13 | Then I should see "Valera Petrov" in "Team user name" 14 | 15 | Scenario: Client header channel name content 16 | Then I should see "#general" in "Channel title" 17 | 18 | Scenario: Client header favorite button 19 | Then I should see icon "Favorite" in "Channel header information" 20 | 21 | Scenario: Client header members button 22 | Then I should see icon "Users" in "Channel header information" 23 | And I should see "2" in "Channel members count" 24 | 25 | Scenario: Empty message container for "general" channel 26 | Then I should see "0" messages 27 | 28 | Scenario: Client header topic 29 | Then I should see "Talk about anything!" in "Channel header topic" 30 | 31 | Scenario: Client header controls 32 | Then I should see the following controls in "Channel header buttons": 33 | | control name | 34 | | Recent mentions button | 35 | | Stars toggle button | 36 | | Flex toggle button | 37 | 38 | Scenario: Input message controls 39 | Then I should see the following controls in "Input message container": 40 | | control name | 41 | | Input message | 42 | | File upload button | 43 | | Emoji button | 44 | | User mentions button | 45 | 46 | Scenario: Notification controls 47 | Then I should see the following controls in "Notification bar container": 48 | | control name | 49 | | Left notification section | 50 | | Right notification section | 51 | 52 | Scenario: Right notification section 53 | Then I should see "*bold* _italics_ ~strike~ `code` ```preformatted``` >quote" in "Right notification section" 54 | 55 | Scenario: Channels content 56 | Then I should see following channels between "Channels" and "Add a channel": 57 | | channel name | 58 | | general | 59 | | random | 60 | 61 | Scenario: Direct channels content 62 | Then I should see following channels between "Direct Messages" and "Invite people": 63 | | channel name | 64 | | Valera Petrov | 65 | | Slackbot | 66 | 67 | Scenario: App channels content 68 | Then I should see following channels between "Apps" and "": 69 | | channel name | 70 | | Valera | 71 | 72 | Scenario: Default selected channel 73 | Then I should see "general" as selected channel -------------------------------------------------------------------------------- /features/preformatted_message_formatting.feature: -------------------------------------------------------------------------------- 1 | Feature: Preformatted message formatting 2 | As a user, I want to send some words or all message as preformatted 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Only one word as preformatted 10 | And I type "```preformatted```" 11 | When I press the "Enter" keyboard button 12 | Then I should see "preformatted" in "Message body" 13 | And Message has the following HTML content at "last" position in "Message body": 14 | | html content | 15 | |
preformatted
| 16 | 17 | Scenario: Two words as preformatted 18 | And I type "```one two```" 19 | When I press the "Enter" keyboard button 20 | Then I should see "one two" in "Message body" 21 | And Message has the following HTML content at "last" position in "Message body": 22 | | html content | 23 | |
one two
| 24 | 25 | Scenario: Two words on different lines 26 | And I type "```one " 27 | And I press the "Shift + Enter" keyboard button 28 | And I type "two```" 29 | When I press the "Enter" keyboard button 30 | Then I should see "last" multiline message with: 31 | | Message body | one \ntwo | 32 | And Message has the following HTML content at "last" position in "Message body": 33 | | html content | 34 | |
one 
two
| 35 | 36 | Scenario: Two words on different lines with breaklines in the middle 37 | And I type "```one " 38 | And I press the "Shift + Enter" keyboard button 39 | And I press the "Shift + Enter" keyboard button 40 | And I press the "Shift + Enter" keyboard button 41 | And I type "two```" 42 | When I press the "Enter" keyboard button 43 | Then I should see "last" multiline message with: 44 | | Message body | one \n\ntwo | 45 | And Message has the following HTML content at "last" position in "Message body": 46 | | html content | 47 | |
one 

two
| 48 | 49 | Scenario: Two words on different lines as preformatted 50 | And I type "```one```" 51 | And I press the "Shift + Enter" keyboard button 52 | And I type "```two```" 53 | When I press the "Enter" keyboard button 54 | Then I should see "last" multiline message with: 55 | | Message body | one\ntwo | 56 | And Message has the following HTML content at "last" position in "Message body": 57 | | html content | 58 | |
one
two
| 59 | 60 | Scenario: No breaklines immediately after a preformatted start block 61 | And I type "```" 62 | And I press the "Shift + Enter" keyboard button 63 | And I type "one```" 64 | When I press the "Enter" keyboard button 65 | Then I should see "last" multiline message with: 66 | | Message body | one | 67 | And Message has the following HTML content at "last" position in "Message body": 68 | | html content | 69 | |
one
| 70 | 71 | Scenario: No breaklines before a preformatted end block 72 | And I type "```" 73 | And I press the "Shift + Enter" keyboard button 74 | And I type "one" 75 | And I press the "Shift + Enter" keyboard button 76 | And I type "```" 77 | When I press the "Enter" keyboard button 78 | Then I should see "last" multiline message with: 79 | | Message body | one | 80 | And Message has the following HTML content at "last" position in "Message body": 81 | | html content | 82 | |
one
| 83 | 84 | Scenario: Display two preformatted blocks separated by text 85 | And I type "```" 86 | And I press the "Shift + Enter" keyboard button 87 | And I type "one" 88 | And I press the "Shift + Enter" keyboard button 89 | And I type "```" 90 | And I press the "Shift + Enter" keyboard button 91 | And I type "some text" 92 | And I press the "Shift + Enter" keyboard button 93 | And I type "```" 94 | And I press the "Shift + Enter" keyboard button 95 | And I type "two" 96 | And I press the "Shift + Enter" keyboard button 97 | And I type "```" 98 | When I press the "Enter" keyboard button 99 | Then I should see "last" multiline message with: 100 | | Message body | one\nsome text\n\ntwo | 101 | And Message has the following HTML content at "last" position in "Message body": 102 | | html content | 103 | |
one
some text
two
| 104 | 105 | Scenario: Display two preformatted blocks separated by breaklines 106 | And I type "```" 107 | And I press the "Shift + Enter" keyboard button 108 | And I type "one" 109 | And I press the "Shift + Enter" keyboard button 110 | And I type "```" 111 | And I press the "Shift + Enter" keyboard button 112 | And I press the "Shift + Enter" keyboard button 113 | And I press the "Shift + Enter" keyboard button 114 | And I type "```" 115 | And I press the "Shift + Enter" keyboard button 116 | And I type "two" 117 | And I press the "Shift + Enter" keyboard button 118 | And I type "```" 119 | When I press the "Enter" keyboard button 120 | Then I should see "last" multiline message with: 121 | | Message body | one\ntwo | 122 | And Message has the following HTML content at "last" position in "Message body": 123 | """ 124 |
one
two
125 | """ 126 | 127 | Scenario: Preserve spaces at start and at the end of block and between words 128 | And I type "``` one two three ```" 129 | When I press the "Enter" keyboard button 130 | Then I should see "last" multiline message with: 131 | | Message body | one two three | 132 | And Message has the following HTML content at "last" position in "Message body": 133 | | html content | 134 | |
    one two  three 
| 135 | 136 | Scenario: Exclude preformatting with only begin block 137 | And I type "```one two" 138 | When I press the "Enter" keyboard button 139 | Then I should see "```one two" in "Message body" 140 | And Message has the following HTML content at "last" position in "Message body": 141 | | html content | 142 | | ```one two | 143 | 144 | Scenario: Exclude preformatting with only end block 145 | And I type "one two```" 146 | When I press the "Enter" keyboard button 147 | Then I should see "one two```" in "Message body" 148 | And Message has the following HTML content at "last" position in "Message body": 149 | | html content | 150 | | one two``` | 151 | 152 | Scenario: Exclude preformatting with no content 153 | And I type "``````" 154 | When I press the "Enter" keyboard button 155 | Then I should see "``````" in "Message body" 156 | And Message has the following HTML content at "last" position in "Message body": 157 | | html content | 158 | | `````` | 159 | 160 | Scenario: Exclude preformatting with spaces content 161 | And I type "``` ```" 162 | When I press the "Enter" keyboard button 163 | Then I should see "``` ```" in "Message body" 164 | And Message has the following HTML content at "last" position in "Message body": 165 | | html content | 166 | | ``` ``` | 167 | 168 | Scenario: Exclude preformatting with breaklines content 169 | And I type "```" 170 | And I press the "Shift + Enter" keyboard button 171 | And I press the "Shift + Enter" keyboard button 172 | And I press the "Shift + Enter" keyboard button 173 | And I press the "Shift + Enter" keyboard button 174 | And I press the "Shift + Enter" keyboard button 175 | And I type "```" 176 | When I press the "Enter" keyboard button 177 | Then I should see "``````" in "Message body" 178 | And Message has the following HTML content at "last" position in "Message body": 179 | | html content | 180 | | `````` | -------------------------------------------------------------------------------- /features/quote_message_formatting.feature: -------------------------------------------------------------------------------- 1 | Feature: Quote message formatting 2 | As a user, I want to send some words or all message in quote 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Only one word in blockquote 10 | And I type ">quote" 11 | When I press the "Enter" keyboard button 12 | Then I should see "quote" in "Message body" 13 | And Message has the following HTML content at "last" position in "Message body": 14 | | html content | 15 | |
quote
| 16 | 17 | Scenario: More than one word in blockquote 18 | And I type ">quote with more than one word" 19 | When I press the "Enter" keyboard button 20 | Then I should see "quote with more than one word" in "Message body" 21 | And Message has the following HTML content at "last" position in "Message body": 22 | | html content | 23 | |
quote with more than one word
| 24 | 25 | Scenario: Multiline blockquote formatting without breaklines 26 | And I type ">quote line 1" 27 | And I press the "Shift + Enter" keyboard button 28 | And I type ">quote line 2" 29 | And I press the "Shift + Enter" keyboard button 30 | And I type ">quote line 3" 31 | When I press the "Enter" keyboard button 32 | Then I should see "last" multiline message with: 33 | | Message body | quote line 1\nquote line 2\nquote line 3 | 34 | And Message has the following HTML content at "last" position in "Message body": 35 | | html content | 36 | |
quote line 1
quote line 2
quote line 3
| 37 | 38 | Scenario: Multiline blockquote formatting with breaklines 39 | And I type ">quote line 1" 40 | And I press the "Shift + Enter" keyboard button 41 | And I type ">" 42 | And I press the "Shift + Enter" keyboard button 43 | And I type ">" 44 | And I press the "Shift + Enter" keyboard button 45 | And I type ">" 46 | And I press the "Shift + Enter" keyboard button 47 | And I type ">quote line 2" 48 | And I press the "Shift + Enter" keyboard button 49 | And I type ">quote line 3" 50 | When I press the "Enter" keyboard button 51 | Then I should see "last" multiline message with: 52 | | Message body | quote line 1\n\n\nquote line 2\nquote line 3 | 53 | And Message has the following HTML content at "last" position in "Message body": 54 | | html content | 55 | |
quote line 1


quote line 2
quote line 3
| 56 | 57 | Scenario: Two blockquote lines separated with text 58 | And I type ">quote line 1" 59 | And I press the "Shift + Enter" keyboard button 60 | And I type "some text" 61 | And I press the "Shift + Enter" keyboard button 62 | And I type ">quote line 2" 63 | When I press the "Enter" keyboard button 64 | Then I should see "last" multiline message with: 65 | | Message body | quote line 1\nsome text\n\nquote line 2 | 66 | And Message has the following HTML content at "last" position in "Message body": 67 | | html content | 68 | |
quote line 1
some text
quote line 2
| 69 | 70 | Scenario: Do not render next lines as blockquote if starts with spaces 71 | And I type ">quote line 1" 72 | And I press the "Shift + Enter" keyboard button 73 | And I type " >not blockquote 1" 74 | And I press the "Shift + Enter" keyboard button 75 | And I type " >not blockquote 2" 76 | When I press the "Enter" keyboard button 77 | Then I should see "last" multiline message with: 78 | | Message body | quote line 1\n>not blockquote 1\n >not blockquote 2 | 79 | And Message has the following HTML content at "last" position in "Message body": 80 | | html content | 81 | |
quote line 1
>not blockquote 1
  >not blockquote 2 | 82 | 83 | Scenario: Preserve spaces inside quote 84 | And I type "> quote line 1" 85 | When I press the "Enter" keyboard button 86 | Then I should see "last" multiline message with: 87 | | Message body | quote line 1 | 88 | And Message has the following HTML content at "last" position in "Message body": 89 | | html content | 90 | |
  quote line 1
| 91 | 92 | Scenario: Preserve spaces inside multiline quote 93 | And I type "> quote line 1" 94 | And I press the "Shift + Enter" keyboard button 95 | And I type "> quote line 2" 96 | When I press the "Enter" keyboard button 97 | Then I should see "last" multiline message with: 98 | | Message body | quote line 1\n quote line 2 | 99 | And Message has the following HTML content at "last" position in "Message body": 100 | | html content | 101 | |
 quote line 1
   quote line 2
| -------------------------------------------------------------------------------- /features/receiving_messages.feature: -------------------------------------------------------------------------------- 1 | Feature: Receiving messages 2 | As a user, I want to receive messages from different channels 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | And User "Valera" connected to fake slack using parameters: 9 | | token | xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT | 10 | | url | http://localhost:9001/api/ | 11 | 12 | Scenario: Reciving "typing" status message in the "general" channel 13 | When User "Valera" send "user_typing" message to "general" channel 14 | Then I should see "Valera is typing" in "Notification bar item" 15 | 16 | Scenario: Reciving "typing" status message in the "random" channel 17 | And I click on "channel item" with text "random" 18 | When User "Valera" send "user_typing" message to "random" channel 19 | Then I should see "Valera is typing" in "Notification bar item" 20 | 21 | Scenario: Receiving simple text message from "general" channel 22 | When User "Valera" send message: 23 | | type | message | 24 | | text | Text from bot #1 | 25 | | channel | general | 26 | Then I should see "last" message with: 27 | | Message sender | Valera | 28 | | App badge | APP | 29 | | Message body | Text from bot #1 | 30 | 31 | Scenario: Receiving simple text message from "random" channel 32 | And I click on "channel item" with text "random" 33 | When User "Valera" send message: 34 | | type | message | 35 | | text | Text from bot #2 | 36 | | channel | random | 37 | Then I should see "last" message with: 38 | | Message sender | Valera | 39 | | App badge | APP | 40 | | Message body | Text from bot #2 | -------------------------------------------------------------------------------- /features/sending_messages.feature: -------------------------------------------------------------------------------- 1 | Feature: Sending messages 2 | As a user, I want to send messages to different channels 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | And User "Valera" connected to fake slack using parameters: 9 | | token | xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT | 10 | | url | http://localhost:9001/api/ | 11 | 12 | Scenario: Sending simple text message to "general" channel 13 | And I type "my simple text message" 14 | When I press the "Enter" keyboard button 15 | Then I should see "my simple text message" in "Message body" 16 | And User "Valera" should receive messages: 17 | | message | channel | 18 | | my simple text message | general | 19 | 20 | Scenario: Sending simple text message to "random" channel 21 | And I click on "channel item" with text "random" 22 | And I type "text message to random channel" 23 | When I press the "Enter" keyboard button 24 | Then I should see "text message to random channel" in "Message body" on the "last" position 25 | And User "Valera" should receive messages: 26 | | message | channel | 27 | | text message to random channel | random | 28 | @skip 29 | Scenario: Sending user_typing message to "random" channel 30 | And I click on "channel item" with text "random" 31 | When I type "m" 32 | And I'll wait a little 33 | Then User "Valera" should receive status messages: 34 | | type | channel | 35 | | user_typing | random | 36 | 37 | Scenario: Sending user_typing message to "general" channel 38 | When I type "m" 39 | And I'll wait a little 40 | Then User "Valera" should receive status messages: 41 | | type | channel | 42 | | user_typing | general | -------------------------------------------------------------------------------- /features/step_definitions/hooks.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | const { 3 | After, 4 | AfterAll, 5 | Before, 6 | BeforeAll, 7 | setDefaultTimeout, 8 | Status 9 | } = require('cucumber'); 10 | 11 | const path = require('path'); 12 | const Promise = require('bluebird'); 13 | const scope = require('./support/scope'); 14 | const services = require('./support/services'); 15 | setDefaultTimeout(2 * 60 * 1000); 16 | 17 | async function cleanupAppUsers() { 18 | if (scope.context.appUsers) { 19 | await Promise.mapSeries(Object.entries(scope.context.appUsers), ([, bot]) => bot && bot.close()); 20 | } 21 | scope.context.appUsers = {}; 22 | } 23 | 24 | async function cleanupPage() { 25 | // Here we check if a scenario has instantiated a browser and a current page 26 | if (scope.browser && scope.context.currentPage) { 27 | // if it has, find all the cookies, and delete them 28 | const cookies = await scope.context.currentPage.cookies(); 29 | if (cookies && cookies.length > 0) { 30 | await scope.context.currentPage.deleteCookie(...cookies); 31 | } 32 | await scope.context.currentPage.evaluate(() => { 33 | localStorage.clear(); 34 | }); 35 | // close the web page down 36 | await scope.context.currentPage.close(); 37 | // wipe the context's currentPage value 38 | scope.context.currentPage = null; 39 | } 40 | } 41 | 42 | BeforeAll(async () => { 43 | await services.ui.server.start(); 44 | }); 45 | 46 | Before(async () => { 47 | await cleanupAppUsers(); 48 | await cleanupPage(); 49 | }); 50 | 51 | After(async (testCase) => { 52 | if (scope.browser && testCase.result.status === Status.FAILED) { 53 | const pages = await scope.browser.pages(); 54 | await Promise.mapSeries(pages, 55 | p => p.screenshot( 56 | { 57 | path: path.join(process.cwd(), 'screenshots', `failed${Date.now()}.png`), 58 | fullPage: true 59 | } 60 | )); 61 | } 62 | await cleanupAppUsers(); 63 | await cleanupPage(); 64 | }); 65 | 66 | AfterAll(async () => { 67 | if (scope.browser) await scope.browser.close(); 68 | if (services.ui.server) await services.ui.server.close(); 69 | }); 70 | 71 | async function clearResources() { 72 | if (services.ui.server) await services.ui.server.close(); 73 | } 74 | 75 | process.on('exit', clearResources); 76 | process.on('SIGINT', clearResources); 77 | process.on('SIGTERM', clearResources); 78 | -------------------------------------------------------------------------------- /features/step_definitions/pages.js: -------------------------------------------------------------------------------- 1 | const pages = { 2 | 'fake slack ui': '/' 3 | }; 4 | 5 | module.exports = pages; 6 | -------------------------------------------------------------------------------- /features/step_definitions/selectors.js: -------------------------------------------------------------------------------- 1 | const mainSelectors = { 2 | 'Team name': '#team_name', 3 | 'Team user name': '#team_menu_user_name', 4 | 'Selected channel': '.p-channel_sidebar__channel--selected', 5 | 'Channel title': '#channel_title', 6 | Favorite: '.im.im-star-o', 7 | 'Channel header information': '#channel_header_info', 8 | Users: '.im.im-user-male', 9 | 'Channel members count': '#channel_members_toggle_count', 10 | 'Channel header topic': '#channel_topic_text', 11 | 'Channel header buttons': '.channel_header__buttons', 12 | 'Recent mentions button': 'button#recent_mentions_toggle > .im.im-link', 13 | 'Stars toggle button': 'button#stars_toggle > .im.im-star-o', 14 | 'Flex toggle button': 'button#flex_menu_toggle > .im.im-menu-dot-v', 15 | 'Messages container': '#messages_container', 16 | 'Message item': '.list__item', 17 | 'Input message container': '#msg_form', 18 | 'Input message': '#msg_input_text', 19 | 'File upload button': '#primary_file_button', 20 | 'Emoji button': '#main_emo_menu', 21 | 'User mentions button': '.msg_mentions_button', 22 | 'Notification bar container': '#notification_bar', 23 | 'Left notification section': '.p-notification_bar__section.p-notification_bar__section--left', 24 | 'Right notification section': '.p-notification_bar__section.p-notification_bar__section--right', 25 | 'Message body': '#messages_container .c-message__body', 26 | 'channel item': 'span.p-channel_sidebar__name', 27 | 'Notification bar': '.p-notification_bar__section--left', 28 | 'Notification bar item': '.p-notification_bar__section--left .p-notification_bar__typing', 29 | 'Message sender': '#messages_container .c-message__sender_link', 30 | 'App badge': '#messages_container .c-app_badge', 31 | button: '.c-button', 32 | 'Inline Message Editor': '.c-message__editor' 33 | }; 34 | 35 | const selectors = { 36 | 'fake slack ui': mainSelectors 37 | }; 38 | 39 | module.exports = selectors; 40 | -------------------------------------------------------------------------------- /features/step_definitions/support/fakeuser.js: -------------------------------------------------------------------------------- 1 | let mockDate = null; 2 | 3 | global.OrigDate = global.Date; 4 | global.Date = class MockDate extends global.Date { 5 | constructor(date) { 6 | if (!MockDate.mockedDate) { 7 | return super(date); 8 | } 9 | return super(MockDate.mockedDate.getTime()); 10 | } 11 | 12 | static mockDateIncreaseMinutes(minutes = 0) { 13 | if (MockDate.mockedDate) { 14 | MockDate.mockedDate.setMinutes(MockDate.mockedDate.getMinutes() + minutes); 15 | } 16 | } 17 | 18 | static now() { 19 | if (MockDate.mockedDate) { 20 | return MockDate.mockedDate.getTime(); 21 | } 22 | return global.OrigDate.now(); 23 | } 24 | 25 | static mockDate(date) { 26 | MockDate.mockedDate = date; 27 | } 28 | 29 | static unmockDate() { 30 | MockDate.mockedDate = null; 31 | } 32 | }; 33 | 34 | if (mockDate) { 35 | global.Date.mockDate(mockDate); 36 | } else { 37 | global.Date.unmockDate(); 38 | } 39 | 40 | const { WebClient } = require('@slack/web-api'); 41 | const { RTMClient } = require('@slack/rtm-api'); 42 | const API_CHANNELS_LIST = 'channels.list'; 43 | 44 | class FakeUser { 45 | constructor(token, slackApiUrl) { 46 | this.token = token; 47 | this.slackApiUrl = slackApiUrl; 48 | this.web = null; 49 | this.rtm = null; 50 | this.self = null; 51 | this.team = null; 52 | this.apiResponses = {}; 53 | this.incomingLastUpdate = null; 54 | this.rtmIncomingMessages = []; 55 | } 56 | 57 | async start() { 58 | this.web = new WebClient(this.token, { slackApiUrl: this.slackApiUrl }); 59 | this.rtm = new RTMClient(this.token, { slackApiUrl: this.slackApiUrl }); 60 | this.setupEvents(); 61 | const { self, team } = await this.rtm.start(); 62 | this.self = self; 63 | this.team = team; 64 | return new Promise(resolve => { 65 | this.resolveStart = resolve; 66 | }); 67 | } 68 | 69 | // eslint-disable-next-line class-methods-use-this 70 | setMockDate(isoDate) { 71 | global.Date.mockDate(isoDate); 72 | } 73 | 74 | // eslint-disable-next-line class-methods-use-this 75 | increaseMockDateByMinutes(minutes = 0) { 76 | global.Date.mockDateIncreaseMinutes(minutes); 77 | } 78 | 79 | getChannelIdByName(channelName) { 80 | const result = this.apiResponses[API_CHANNELS_LIST].channels.filter(({ name }) => name === channelName); 81 | return result.length ? result[0] : null; 82 | } 83 | 84 | setupEvents() { 85 | this.rtm.on('ready', async () => { 86 | const authTestResponse = await this.web.auth.test(); 87 | this.apiResponses['auth.test'] = authTestResponse; 88 | const channelsListResponse = await this.web.channels.list({ exclude_archived: 1 }); 89 | this.apiResponses['channels.list'] = channelsListResponse; 90 | if (typeof this.resolveStart === 'function') { 91 | this.resolveStart(); 92 | delete this.resolveStart; 93 | } 94 | }); 95 | 96 | this.rtm.on('message', async (event) => { 97 | this.rtmIncomingMessages.push(event); 98 | }); 99 | 100 | this.rtm.on('user_typing', async (event) => { 101 | this.rtmIncomingMessages.push(event); 102 | }); 103 | } 104 | 105 | sendTextMessageToChannel(channelName, message) { 106 | const channel = this.getChannelIdByName(channelName); 107 | return this.rtm.sendMessage(message, channel.id); 108 | } 109 | 110 | sendUserTypingToChannel(channelName) { 111 | const channel = this.getChannelIdByName(channelName); 112 | return this.rtm.sendTyping(channel.id); 113 | } 114 | 115 | getLastIncomingPayload(payloadType) { 116 | const lastItemIndex = this.rtmIncomingMessages.length - 1; 117 | const index = this.rtmIncomingMessages.slice().reverse().findIndex(m => m.type === payloadType); 118 | return index >= 0 && this.rtmIncomingMessages[lastItemIndex - index]; 119 | } 120 | 121 | getLastIncomingMessage() { 122 | return this.getLastIncomingPayload('message'); 123 | } 124 | 125 | getLastIncomingMessageByChannelId(channelId) { 126 | const filteredMessages = this.getLastIncomingMessagesByChannelId(channelId, 1); 127 | return filteredMessages[filteredMessages.length - 1]; 128 | } 129 | 130 | getLastIncomingMessagesByChannelId(channelId, limit = 10) { 131 | const filteredMessages = this.rtmIncomingMessages.filter(msg => msg.channel === channelId); 132 | return filteredMessages.slice(-1 * limit); 133 | } 134 | 135 | close() { 136 | if (this.rtm) { 137 | this.rtm.removeAllListeners(); 138 | return this.rtm.disconnect(); 139 | } 140 | return true; 141 | } 142 | } 143 | 144 | module.exports = FakeUser; 145 | -------------------------------------------------------------------------------- /features/step_definitions/support/scope.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /features/step_definitions/support/services.js: -------------------------------------------------------------------------------- 1 | const createUIServer = require('../../../server'); 2 | const FakeUser = require('./fakeuser'); 3 | 4 | module.exports = { 5 | ui: { 6 | server: createUIServer({ 7 | httpPort: 9001, 8 | httpHost: 'localhost' 9 | }) 10 | }, 11 | user: { 12 | create({ token, url }) { 13 | return new FakeUser(token, url); 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /features/step_definitions/support/validators.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CustomJSONSchemaValidator(Validator) { 3 | /* eslint-disable no-param-reassign */ 4 | Validator.prototype.customFormats.uuid = function uuid(input) { 5 | const v4uuid = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i); 6 | return !!String(input).match(v4uuid); 7 | }; 8 | 9 | Validator.prototype.customFormats.slackts = function slackts(input) { 10 | return !!String(input).match(/^([0-9]{10})\.([0-9]{6})$/); 11 | }; 12 | 13 | Validator.prototype.customFormats.slackchid = function slackchid(input) { 14 | return !!String(input).match(/^C[0-9A-Z]{8}$/); 15 | }; 16 | 17 | Validator.prototype.customFormats.slackuid = function slackuid(input) { 18 | return !!String(input).match(/^(U|W)[0-9A-Z]{8}$/); 19 | }; 20 | 21 | Validator.prototype.customFormats.slacktid = function slacktid(input) { 22 | return !!String(input).match(/^T[0-9A-Z]{8}$/); 23 | }; 24 | 25 | Validator.prototype.customFormats.slackmtypes = function slackmtypes(input) { 26 | return ['message', 'user_typing'].includes(String(input)); 27 | }; 28 | 29 | Validator.prototype.customFormats.slackmsubtypes = function slackmtypes(input) { 30 | return ['message_changed'].includes(String(input)); 31 | }; 32 | 33 | /* eslint-enable no-param-reassign */ 34 | return Validator; 35 | }, 36 | CustomJSONSchemaValidatorAddRefs(validatorInstance) { 37 | const ChatUpdateMessageEditedSchema = { 38 | id: '/ChatUpdateMessageEdited', 39 | type: 'object', 40 | properties: { 41 | user: { type: 'string', format: 'slackuid' }, 42 | ts: { type: 'string', format: 'slackts' } 43 | }, 44 | required: ['user', 'ts'] 45 | }; 46 | validatorInstance.addSchema(ChatUpdateMessageEditedSchema, ChatUpdateMessageEditedSchema.id); 47 | 48 | const ChatUpdateMessageSchema = { 49 | id: '/ChatUpdateMessage', 50 | type: 'object', 51 | properties: { 52 | client_msg_id: { type: 'string', format: 'uuid' }, 53 | type: { type: 'string', format: 'slackmtypes' }, 54 | user: { type: 'string', format: 'slackuid' }, 55 | team: { type: 'string', format: 'slacktid' }, 56 | edited: { $ref: '/ChatUpdateMessageEdited' }, 57 | user_team: { type: 'string', format: 'slacktid' }, 58 | source_team: { type: 'string', format: 'slacktid' }, 59 | channel: { type: 'string', format: 'slackchid' }, 60 | text: { type: 'string' }, 61 | ts: { type: 'string', format: 'slackts' } 62 | }, 63 | required: ['client_msg_id', 'type', 'user', 'team', 'edited', 'user_team', 'source_team', 'channel', 'text', 'ts'] 64 | }; 65 | validatorInstance.addSchema(ChatUpdateMessageSchema, ChatUpdateMessageSchema.id); 66 | 67 | const ChatUpdatePreviousMessageSchema = { 68 | id: '/ChatUpdatePreviousMessage', 69 | type: 'object', 70 | properties: { 71 | client_msg_id: { type: 'string', format: 'uuid' }, 72 | type: { type: 'string', format: 'slackmtypes' }, 73 | user: { type: 'string', format: 'slackuid' }, 74 | team: { type: 'string', format: 'slacktid' }, 75 | text: { type: 'string' }, 76 | ts: { type: 'string', format: 'slackts' } 77 | }, 78 | required: ['client_msg_id', 'type', 'user', 'team', 'ts', 'text'] 79 | }; 80 | validatorInstance.addSchema(ChatUpdatePreviousMessageSchema, ChatUpdatePreviousMessageSchema.id); 81 | 82 | return validatorInstance; 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /features/step_definitions/world.js: -------------------------------------------------------------------------------- 1 | const { 2 | setWorldConstructor 3 | } = require('cucumber'); 4 | 5 | const puppeteer = require('puppeteer'); 6 | const scope = require('./support/scope'); 7 | 8 | // eslint-disable-next-line func-names 9 | function World() { 10 | scope.host = 'http://localhost:9001'; 11 | scope.driver = puppeteer; 12 | scope.context = {}; 13 | scope.memo = {}; 14 | scope.interceptRequests = {}; 15 | scope.locale = {}; 16 | scope.locale.languages = ['en-US', 'en']; 17 | scope.locale.language = ['en-US']; 18 | scope.locale.timeZone = 'Asia/Almaty'; 19 | scope.mockDate = null; 20 | } 21 | 22 | setWorldConstructor(World); 23 | -------------------------------------------------------------------------------- /features/strike_message_formatting.feature: -------------------------------------------------------------------------------- 1 | Feature: Strike message formatting 2 | As a user, I want to send some words or all message in strikethrough style 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | 9 | Scenario: Only one word in strike 10 | And I type "~strike~" 11 | When I press the "Enter" keyboard button 12 | Then I should see "strike" in "Message body" 13 | And Message has the following HTML content at "last" position in "Message body": 14 | | html content | 15 | | strike | 16 | 17 | Scenario: Two words in strike sequentially 18 | And I type "~strike1~ ~strike2~" 19 | When I press the "Enter" keyboard button 20 | Then I should see "strike1 strike2" in "Message body" 21 | And Message has the following HTML content at "last" position in "Message body": 22 | | html content | 23 | | strike1 strike2 | 24 | 25 | Scenario: Word surrounded with tildes which between 2 tildes 26 | And I type "~ ~strike~ ~" 27 | When I press the "Enter" keyboard button 28 | Then I should see "~ strike ~" in "Message body" 29 | And Message has the following HTML content at "last" position in "Message body": 30 | | html content | 31 | | ~ strike ~ | 32 | 33 | Scenario: Formatting with preserving spaces at the beginning 34 | And I type "~ strike~" 35 | When I press the "Enter" keyboard button 36 | Then I should see "strike" in "Message body" 37 | And Message has the following HTML content at "last" position in "Message body": 38 | | html content | 39 | |        strike | 40 | 41 | Scenario: Formatting with more than one word 42 | And I type "~ strike line with spaces in it~" 43 | When I press the "Enter" keyboard button 44 | Then I should see "strike line with spaces in it" in "Message body" 45 | And Message has the following HTML content at "last" position in "Message body": 46 | | html content | 47 | |   strike line  with spaces   in    it | 48 | 49 | Scenario: No strike formatting for word with space before ending tilda 50 | And I type "~ strike ~" 51 | When I press the "Enter" keyboard button 52 | Then I should see "~ strike ~" in "Message body" 53 | And Message has the following HTML content at "last" position in "Message body": 54 | | html content | 55 | | ~       strike ~ | 56 | 57 | Scenario: Strike formatting of two words on different lines 58 | And I type "~first~" 59 | When I press the "Shift + Enter" keyboard button 60 | And I type "~second~" 61 | When I press the "Enter" keyboard button 62 | Then I should see "last" multiline message with: 63 | | Message body | first\nsecond | 64 | And Message has the following HTML content at "last" position in "Message body": 65 | | html content | 66 | | first
second | 67 | 68 | Scenario: Skip strike formatting if starts from double tilde 69 | And I type "~~first~" 70 | When I press the "Enter" keyboard button 71 | Then I should see "last" multiline message with: 72 | | Message body | ~~first~ | 73 | And Message has the following HTML content at "last" position in "Message body": 74 | | html content | 75 | | ~~first~ | 76 | 77 | Scenario: Skip strike formatting if starts from double tilde and spaces 78 | And I type "~~ first~" 79 | When I press the "Enter" keyboard button 80 | Then I should see "last" multiline message with: 81 | | Message body | ~~ first~ | 82 | And Message has the following HTML content at "last" position in "Message body": 83 | | html content | 84 | | ~~   first~ | 85 | 86 | Scenario: Only many tildes 87 | And I type "~~~~~~~~~~~~~~~~" 88 | When I press the "Enter" keyboard button 89 | Then I should see "~~~~~~~~~~~~~~~~" in "Message body" 90 | And Message has the following HTML content at "last" position in "Message body": 91 | | html content | 92 | | ~~~~~~~~~~~~~~~~ | 93 | 94 | Scenario: Only tildes separated with spaces 95 | And I type "~~ ~ ~ ~ ~ ~ ~ ~ ~" 96 | When I press the "Enter" keyboard button 97 | Then I should see "~~ ~ ~ ~ ~ ~ ~ ~ ~" in "Message body" 98 | And Message has the following HTML content at "last" position in "Message body": 99 | | html content | 100 | | ~~ ~ ~ ~  ~ ~   ~ ~     ~ | 101 | 102 | Scenario: Word with a tilda at the beginning without a closing tilda 103 | And I type "~strike" 104 | When I press the "Enter" keyboard button 105 | Then I should see "~strike" in "Message body" 106 | And Message has the following HTML content at "last" position in "Message body": 107 | | html content | 108 | | ~strike | 109 | 110 | Scenario: Only 2 tildes 111 | And I type "~~" 112 | When I press the "Enter" keyboard button 113 | Then I should see "~~" in "Message body" 114 | And Message has the following HTML content at "last" position in "Message body": 115 | | html content | 116 | | ~~ | 117 | 118 | Scenario: Only 2 tildes with any spaces between 119 | And I type "~ ~" 120 | When I press the "Enter" keyboard button 121 | Then I should see "~ ~" in "Message body" 122 | And Message has the following HTML content at "last" position in "Message body": 123 | | html content | 124 | | ~     ~ | 125 | 126 | Scenario: Word with a double tildes at the beginning and at the end 127 | And I type "~~strike~~" 128 | When I press the "Enter" keyboard button 129 | Then I should see "~~strike~~" in "Message body" 130 | And Message has the following HTML content at "last" position in "Message body": 131 | | html content | 132 | | ~~strike~~ | 133 | 134 | Scenario: Word with a double tildes at the end 135 | And I type "~strike~~" 136 | When I press the "Enter" keyboard button 137 | Then I should see "~strike~~" in "Message body" 138 | And Message has the following HTML content at "last" position in "Message body": 139 | | html content | 140 | | ~strike~~ | 141 | 142 | Scenario: With breakline separator 143 | And I type "~first line strike" 144 | When I press the "Shift + Enter" keyboard button 145 | And I type "second line strike~" 146 | When I press the "Enter" keyboard button 147 | Then I should see "last" multiline message with: 148 | | Message sender | Valera Petrov | 149 | | Message body | ~first line strike\nsecond line strike~ | 150 | And Message has the following HTML content at "last" position in "Message body": 151 | | html content | 152 | | ~first line strike
second line strike~ | -------------------------------------------------------------------------------- /features/update_message_event.feature: -------------------------------------------------------------------------------- 1 | Feature: Like a bot, I want to receive an event 2 | about updating the contents of the message in the channel 3 | 4 | Background: 5 | Given My timezone is "Asia/Bishkek" 6 | And Fake slack db is empty 7 | And I am on "fake slack ui" page 8 | And User "Valera" connected to fake slack using parameters: 9 | | token | xoxb-XXXXXXXXXXXX-TTTTTTTTTTTTTT | 10 | | url | http://localhost:9001/api/ | 11 | 12 | Scenario: Checking a format of "message_changed" subtype message 13 | And I send "first message" to chat 14 | And I should see "last" multiline "Message item" with: 15 | | Message sender | Valera Petrov | 16 | | Message body | first message | 17 | And I press the "ArrowUp" keyboard button 18 | And I type " edited" 19 | And I press the "Enter" keyboard button 20 | And I'm waiting for "Inline Message Editor" to be hidden 21 | When I should see "first message edited" in "Message body" 22 | Then User "Valera" should receive "incoming" payload with "message" type: 23 | | field | type | required | format | 24 | | type | string | true | slackmtypes | 25 | | subtype | string | true | slackmsubtypes | 26 | | hidden | boolean | true | | 27 | | message | /ChatUpdateMessage | true | | 28 | | channel | string | true | slackchid | 29 | | previous_message | /ChatUpdatePreviousMessage | true | | 30 | | event_ts | string | true | slackts | 31 | | ts | string | true | slackts | 32 | 33 | Scenario: Checking some props of "message_changed" subtype message 34 | And I send "first message" to chat 35 | And I should see "last" multiline "Message item" with: 36 | | Message sender | Valera Petrov | 37 | | Message body | first message | 38 | And I press the "ArrowUp" keyboard button 39 | And I type " edited" 40 | And I press the "Enter" keyboard button 41 | And I'm waiting for "Inline Message Editor" to be hidden 42 | Then I should see "first message edited" in "Message body" 43 | And User "Valera" should receive "incoming" payload of type "message" with fields: 44 | | field | value | 45 | | type | message | 46 | | subtype | message_changed | 47 | | message.text | first message edited | 48 | | previous_message.text | first message | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const clientHelpers = require('./public/js/helpers.js'); 4 | 5 | module.exports = { 6 | ...clientHelpers, 7 | include(...args) { 8 | const parts = args.slice(0, -1); 9 | let filePath = path.join('views', ...parts); 10 | if (!filePath.endsWith('.hbs')) { 11 | filePath = `${filePath}.hbs`; 12 | } 13 | return fs.readFileSync(filePath, { encoding: 'utf-8' }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | launch: { 3 | args: ['--no-sandbox', '--disable-setuid-sandbox', '--window-size=1920,1080'], 4 | executablePath: 'google-chrome-stable' 5 | }, 6 | server: { 7 | command: 'npm start', 8 | port: 9001, 9 | usedPortAction: 'error', 10 | launchTimeout: 50000 11 | }, 12 | browserContext: 'incognito' 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mad-fake-slack", 3 | "version": "1.0.0", 4 | "description": "Fake Slack implementation on node.js + express.js + express-ws", 5 | "main": "server.js", 6 | "jest": { 7 | "preset": "jest-puppeteer", 8 | "verbose": true, 9 | "testMatch": [ 10 | "**/__tests__/**/?(*.)+(spec|test).[jt]s?(x)", 11 | "**/?(*.)+(spec|test).[jt]s?(x)" 12 | ] 13 | }, 14 | "scripts": { 15 | "test": "npm run test:bdd", 16 | "test:jest": "jest", 17 | "test:bdd": "npm run pretest && npx cucumber-js --fail-fast --tags=\"not @skip\"", 18 | "test:bdd:event": "npm run test:bdd -- -f event-protocol", 19 | "test:bdd:only": "npm run pretest && npx cucumber-js --fail-fast --tags=@only", 20 | "test:bdd:only:slow": "USE_REMOTE_DUBUG=true SLOW_MO=1000 npm run test:bdd:only", 21 | "start": "node server.js", 22 | "example:rtmbot": "SLACK_API=http://localhost:9001/api/ node examples/rtmbot/index.js", 23 | "example:rtmbot:integration": "npm run pretest && SLACK_API=http://0.0.0.0:9001/api/ npm run test:jest -- --runInBand examples/rtmbot/", 24 | "pretest": "eslint --ignore-path .gitignore .", 25 | "lint:hbs": "ember-template-lint views/* views/**/*", 26 | "codeclimate:install": "node scripts/codeclimate/check.js && sh scripts/codeclimate/setup.sh", 27 | "codeclimate:analyze:format:html": "node scripts/codeclimate/check.js && CODECLIMATE_CODE=$(node scripts/volumes/inspect.js) codeclimate analyze -f html > reports/$(date +\"%Y%m%d%H%M%S\").html", 28 | "codeclimate:analyze:format:json": "node scripts/codeclimate/check.js && CODECLIMATE_CODE=$(node scripts/volumes/inspect.js) codeclimate analyze -f json > reports/$(date +\"%Y%m%d%H%M%S\").json", 29 | "codeclimate:analyze:format:text": "node scripts/codeclimate/check.js && CODECLIMATE_CODE=$(node scripts/volumes/inspect.js) codeclimate analyze -f text > reports/$(date +\"%Y%m%d%H%M%S\").txt", 30 | "codeclimate:analyze": "npm run codeclimate:analyze:format:$REPORT_FORMAT", 31 | "glitch:pack": "node ./scripts/glitch.js", 32 | "glitch:unpack": "unzip -o glitch_release_*.zip -d . && rm glitch_release_*.zip && refresh", 33 | "example:rtmbot:glitch": "SLACK_API=http://localhost:3000/api/ node bot.js" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/maddevsio/mad-fake-slack.git" 38 | }, 39 | "keywords": [], 40 | "author": "", 41 | "license": "ISC", 42 | "bugs": { 43 | "url": "https://github.com/maddevsio/mad-fake-slack/issues" 44 | }, 45 | "homepage": "https://github.com/maddevsio/mad-fake-slack#readme", 46 | "dependencies": { 47 | "crypto": "^1.0.1", 48 | "express": "^4.17.1", 49 | "express-handlebars": "^3.1.0", 50 | "express-ws": "^4.0.0", 51 | "handlebars": "^4.3.0", 52 | "moment": "^2.24.0", 53 | "morgan": "^1.9.1", 54 | "multer": "^1.4.1" 55 | }, 56 | "devDependencies": { 57 | "@slack/rtm-api": "^5.0.1", 58 | "@slack/web-api": "^5.0.1", 59 | "archiver": "^3.1.1", 60 | "bluebird": "^3.5.5", 61 | "chalk": "^2.4.2", 62 | "cucumber": "^5.1.0", 63 | "ember-template-lint": "^1.3.0", 64 | "eslint": "^5.16.0", 65 | "eslint-config-airbnb-base": "^13.2.0", 66 | "eslint-plugin-import": "^2.18.0", 67 | "eslint-plugin-jest": "^22.7.1", 68 | "faker": "^4.1.0", 69 | "jest": "^24.8.0", 70 | "jest-puppeteer": "^4.2.0", 71 | "jsonschema": "^1.2.4", 72 | "node-fetch": "^2.6.0", 73 | "puppeteer": "^1.18.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /public/fonts/iconmonstr-iconic-font.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/fonts/iconmonstr-iconic-font.eot -------------------------------------------------------------------------------- /public/fonts/iconmonstr-iconic-font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/fonts/iconmonstr-iconic-font.ttf -------------------------------------------------------------------------------- /public/fonts/iconmonstr-iconic-font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/fonts/iconmonstr-iconic-font.woff -------------------------------------------------------------------------------- /public/fonts/iconmonstr-iconic-font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/fonts/iconmonstr-iconic-font.woff2 -------------------------------------------------------------------------------- /public/images/loading_channel_pane@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/images/loading_channel_pane@2x.png -------------------------------------------------------------------------------- /public/images/meavatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/images/meavatar.png -------------------------------------------------------------------------------- /public/images/slack_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/images/slack_user.png -------------------------------------------------------------------------------- /public/images/uslackbot-avatar-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/public/images/uslackbot-avatar-72.png -------------------------------------------------------------------------------- /public/js/helpers.js: -------------------------------------------------------------------------------- 1 | const isServer = typeof module !== 'undefined'; 2 | let moment; 3 | let Handlebars; 4 | let Formatter; 5 | let getConstants; 6 | 7 | if (isServer) { 8 | /* eslint-disable global-require */ 9 | moment = require('moment'); 10 | Handlebars = require('handlebars'); 11 | Formatter = require('./formatters'); 12 | getConstants = () => require('../../routes/constants'); 13 | /* eslint-enable global-require */ 14 | } else { 15 | moment = window.moment; 16 | Handlebars = window.Handlebars; 17 | Formatter = window.MdFormatter; 18 | getConstants = () => Handlebars.context && Handlebars.context.constants; 19 | } 20 | 21 | function isEmpty(value) { 22 | if (!value && value !== 0) { 23 | return true; 24 | } 25 | 26 | if (Array.isArray(value) && value.length === 0) { 27 | return true; 28 | } 29 | return false; 30 | } 31 | 32 | const escape = { 33 | '&': '&', 34 | '<': '<', 35 | '>': '>', 36 | '"': '"', 37 | "'": ''', 38 | '`': '`', 39 | '=': '=' 40 | }; 41 | 42 | function escapeChar(chr) { 43 | return escape[chr]; 44 | } 45 | 46 | function escapeExpression(string, possible = /[&<"'=]/) { 47 | const badChars = new RegExp(possible, 'g'); 48 | if (typeof string !== 'string') { 49 | // don't escape SafeStrings, since they're already safe 50 | if (string && string.toHTML) { 51 | return string.toHTML(); 52 | } if (string == null) { 53 | return ''; 54 | } if (!string) { 55 | return string + ''; 56 | } 57 | 58 | // Force a string conversion as this will be done by the append regardless and 59 | // the regex test will do this transparently behind the scenes, causing issues if 60 | // an object's to string has escaped characters in it. 61 | // eslint-disable-next-line no-param-reassign 62 | string = '' + string; 63 | } 64 | 65 | if (!possible.test(string)) { return string; } 66 | return string.replace(badChars, escapeChar); 67 | } 68 | 69 | 70 | const helpers = { 71 | id: 0, 72 | json(context) { 73 | return JSON.stringify(context); 74 | }, 75 | concat(...args) { 76 | return args.slice(0, -1).join(''); 77 | }, 78 | eq(v1, v2) { 79 | return v1 === v2; 80 | }, 81 | ne(v1, v2) { 82 | return v1 !== v2; 83 | }, 84 | lt(v1, v2) { 85 | return v1 < v2; 86 | }, 87 | gt(v1, v2) { 88 | return v1 > v2; 89 | }, 90 | lte(v1, v2) { 91 | return v1 <= v2; 92 | }, 93 | gte(v1, v2) { 94 | return v1 >= v2; 95 | }, 96 | and() { 97 | return Array.prototype.slice.call(arguments).every(Boolean); 98 | }, 99 | or() { 100 | return Array.prototype.slice.call(arguments, 0, -1).some(Boolean); 101 | }, 102 | makeTs() { 103 | helpers.id += 1; 104 | return helpers.createTs(helpers.id); 105 | }, 106 | createTs(incrementId) { 107 | const dateNow = Date.now(); 108 | return `${Math.floor(dateNow / 1000)}.${String(incrementId).padStart(6, '0')}`; 109 | }, 110 | toHumanTime(timestamp) { 111 | if (!timestamp) return '00:00'; 112 | const unixts = +timestamp.split('.')[0]; 113 | return moment.unix(unixts).format('h:mm A'); 114 | }, 115 | inlineIf(conditional, trueOption, elseOption) { 116 | let ifCondition = conditional; 117 | if (typeof ifCondition === 'function') { 118 | ifCondition = ifCondition.call(this); 119 | } 120 | if (!conditional || isEmpty(conditional)) { 121 | return elseOption; 122 | } 123 | return trueOption; 124 | }, 125 | formatMessage(text) { 126 | let message = escapeExpression(text); 127 | const formatter = new Formatter(escapeExpression); 128 | message = formatter.format(message); 129 | const trimBr = line => line && line.replace(/^(\r|\n|\r\n)/ig, '').replace(/(\r|\n|\r\n)$/ig, ''); 130 | const format = (line, index, arr) => { 131 | const nextIsEmpty = trimBr(arr[index + 1]) === ''; 132 | const currentIsEmpty = trimBr(line) === ''; 133 | const currentIsPre = line.startsWith('' : ''; 136 | return nextIsEmpty ? trimBr(line) : `${trimBr(line)}${lineBreaker}`; 137 | } 138 | return index > 0 ? '' : ''; 139 | }; 140 | message = trimBr(message).split('\n').map(format).join(''); 141 | return new Handlebars.SafeString(message); 142 | }, 143 | formatInlineMessage(text) { 144 | let message = escapeExpression(text.trim()); 145 | message = message.split('\n').map(line => (line === '' ? '


' : `

${line}

`)).join(''); 146 | return new Handlebars.SafeString(message); 147 | }, 148 | getTsDiffInSeconds(firstTs, secondTs) { 149 | const firstUnixTs = Number(firstTs.split('.')[0]); 150 | const secondUnixTs = Number(secondTs.split('.')[0]); 151 | const maxTs = Math.max(firstUnixTs, secondUnixTs); 152 | const minTs = Math.min(firstUnixTs, secondUnixTs); 153 | return Math.round(maxTs - minTs); 154 | }, 155 | canHideHeader(currentMessage, baseMessage, interval = 0) { 156 | const diffInSeconds = helpers.getTsDiffInSeconds(currentMessage.ts, baseMessage.ts); 157 | return String(baseMessage.user_id) === String(currentMessage.user_id) && diffInSeconds <= interval; 158 | }, 159 | findFirstMessageByUser(messages, userId) { 160 | const keys = Object.keys(messages); 161 | let firstMessageFromUser = null; 162 | for (let i = keys.length - 1; i >= 0; i -= 1) { 163 | const message = messages[keys[i]]; 164 | if (message.user_id !== userId) { 165 | break; 166 | } else { 167 | firstMessageFromUser = message; 168 | } 169 | } 170 | return firstMessageFromUser; 171 | }, 172 | measureMessageItems(prevItem, item) { 173 | const intervalInSeconds = getConstants().HIDE_HEADER_TIME_INTERVAL_IN_SECONDS; 174 | if (prevItem && prevItem.user_id === item.user_id) { 175 | const messageDiffInSeconds = helpers.getTsDiffInSeconds(prevItem.ts, item.ts); 176 | if (messageDiffInSeconds >= intervalInSeconds) { 177 | return [item, { ...item, hideHeader: false }]; 178 | } 179 | const hideHeader = helpers.canHideHeader(item, prevItem, intervalInSeconds); 180 | return [prevItem, { ...item, hideHeader }]; 181 | } 182 | return [item, { ...item, hideHeader: false }]; 183 | }, 184 | eachMessage(context, options) { 185 | let currentContext = context; 186 | let prevItem; 187 | const intervalInSeconds = getConstants().HIDE_HEADER_TIME_INTERVAL_IN_SECONDS; 188 | if (!options) { 189 | throw new Error('Must pass iterator to #eachMessage'); 190 | } 191 | 192 | let fn = options.fn; 193 | let inverse = options.inverse; 194 | let data; 195 | 196 | if (typeof currentContext === 'function') { 197 | currentContext = currentContext.call(this); 198 | } 199 | 200 | if (options.data) { 201 | data = Handlebars.createFrame(options.data); 202 | } 203 | 204 | function execIteration(field, index, last) { 205 | if (data) { 206 | data.key = field; 207 | data.index = index; 208 | data.first = index === 0; 209 | data.last = !!last; 210 | } 211 | let item = currentContext[field]; 212 | [prevItem, item] = helpers.measureMessageItems(prevItem, item, intervalInSeconds); 213 | return fn(item, { 214 | data: data, 215 | blockParams: [item, field] 216 | }); 217 | } 218 | 219 | if (currentContext && typeof currentContext === 'object') { 220 | return Object.keys(currentContext).reduce((acc, key, index, arr) => { 221 | return [acc, execIteration(key, index, arr.length - 1 === index)].join(''); 222 | }, ''); 223 | } 224 | 225 | return inverse(this); 226 | } 227 | }; 228 | 229 | if (isServer) { 230 | module.exports = helpers; 231 | } else if (typeof window.Handlebars !== 'undefined') { 232 | Object.entries(helpers).forEach(([name, fn]) => window.Handlebars.registerHelper(name, fn)); 233 | } 234 | -------------------------------------------------------------------------------- /reports/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/reports/.keep -------------------------------------------------------------------------------- /routes/api/factories.js: -------------------------------------------------------------------------------- 1 | const responses = require('../responses'); 2 | const utils = require('./utils'); 3 | const faker = require('faker'); 4 | let generationId = 1; 5 | let eventIdTracker = 1; 6 | 7 | function createMessageResponse({ 8 | type, 9 | ts, 10 | text, 11 | channel 12 | }, { user, team }) { 13 | const response = utils.copyObject(responses['chat.postMessage']); 14 | const msgTs = typeof ts === 'undefined' ? utils.createTs(generationId) : ts; 15 | generationId += 1; 16 | response.ts = msgTs; 17 | response.channel = channel; 18 | 19 | const overrideProperties = { 20 | client_msg_id: faker.random.uuid(), 21 | text, 22 | type, 23 | user: user.id, 24 | team: team.id, 25 | user_team: team.id, 26 | source_team: team.id, 27 | channel, 28 | event_ts: msgTs, 29 | ts: msgTs 30 | }; 31 | 32 | response.message = { 33 | ...response.message, 34 | ...overrideProperties 35 | }; 36 | return response; 37 | } 38 | 39 | function createUpdateMessageEvent({ channel, message, previousMessage }, { user, team }) { 40 | const eventTs = utils.createTs(eventIdTracker); 41 | eventIdTracker += 1; 42 | const clientMsgId = faker.random.uuid(); 43 | 44 | const overrideMessageProps = { 45 | client_msg_id: clientMsgId, 46 | type: 'message', 47 | user: user.id, 48 | team: team.id, 49 | edited: { user: user.id, ts: eventTs }, 50 | user_team: team.id, 51 | source_team: team.id, 52 | channel 53 | }; 54 | 55 | const overridePevMessageProps = { 56 | client_msg_id: clientMsgId, 57 | type: 'message', 58 | user: user.id, 59 | team: team.id 60 | }; 61 | 62 | const messagePayload = { 63 | ...utils.copyObject(message), 64 | ...overrideMessageProps 65 | }; 66 | 67 | const previousMessagePayload = { 68 | ...utils.copyObject(previousMessage), 69 | ...overridePevMessageProps 70 | }; 71 | 72 | ['user_id'].forEach(unwantedProp => { 73 | delete messagePayload[unwantedProp]; 74 | delete previousMessagePayload[unwantedProp]; 75 | }); 76 | 77 | return { 78 | type: 'message', 79 | subtype: 'message_changed', 80 | hidden: false, 81 | message: messagePayload, 82 | channel, 83 | previous_message: previousMessagePayload, 84 | event_ts: eventTs, 85 | ts: eventTs 86 | }; 87 | } 88 | 89 | module.exports = { 90 | createMessageResponse, 91 | createUpdateMessageEvent 92 | }; 93 | -------------------------------------------------------------------------------- /routes/api/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const crypto = require('crypto'); 4 | const multer = require('multer'); 5 | 6 | const { dbManager, wsManager } = require('../managers'); 7 | const responses = require('../responses'); 8 | const utils = require('./utils'); 9 | const factories = require('./factories'); 10 | 11 | const storage = multer.diskStorage({ 12 | destination: (req, file, cb) => { 13 | cb(null, '/tmp/uploads'); 14 | }, 15 | filename: (req, file, cb) => { 16 | cb(null, `${file.fieldname}-${Date.now()}`); 17 | } 18 | }); 19 | 20 | const upload = multer({ storage }); 21 | 22 | function beforeAllHandler(req, res, next) { 23 | if (utils.isUrlEncodedForm(req)) { 24 | express.urlencoded()(req, res, next); 25 | } else if (utils.isMultipartForm(req)) { 26 | upload.none()(req, res, next); 27 | } else { 28 | express.json()(req, res, next); 29 | } 30 | } 31 | 32 | function authTestHandler(req, res) { 33 | const token = (req.body && req.body.token) || req.headers.Authorization || req.headers.authorization; 34 | if (!token) { 35 | res.json(responses.invalid_auth); 36 | return; 37 | } 38 | 39 | const uid = crypto 40 | .createHash('md5') 41 | .update(token) 42 | .digest('hex'); 43 | 44 | const users = dbManager.users().findById(dbManager.db.sessions[uid]); 45 | if (!users.length) { 46 | res.json(responses.invalid_auth); 47 | } else { 48 | const user = users[0]; 49 | const team = dbManager.db.teams.filter(tm => tm.id === user.team_id)[0]; 50 | const exampleResponse = responses['auth.test']; 51 | exampleResponse.team_id = team.id; 52 | exampleResponse.user_id = user.id; 53 | const schema = process.env.URL_SCHEMA || 'http'; 54 | exampleResponse.url = `${schema}://${team.domain}/`; 55 | exampleResponse.team = team.name || exampleResponse.team; 56 | exampleResponse.user = user.name || exampleResponse.team; 57 | res.json(exampleResponse); 58 | } 59 | } 60 | 61 | function createResponse({ 62 | user, 63 | team, 64 | ...other 65 | }) { 66 | return factories.createMessageResponse( 67 | { 68 | type: 'message', 69 | ...other 70 | }, 71 | { user, team } 72 | ); 73 | } 74 | 75 | function broadcastResponse({ payload, channel, userId }) { 76 | wsManager.broadcast(JSON.stringify(payload), userId); 77 | if (utils.isOpenChannel(channel)) { 78 | wsManager.broadcastToBots(JSON.stringify(payload), userId); 79 | } 80 | if (utils.isBot(channel, dbManager)) { 81 | wsManager.broadcastToBot(JSON.stringify(payload), channel); 82 | } 83 | } 84 | 85 | async function postMessageHandler(req, res) { 86 | const message = dbManager.channel(req.body.channel).createMessage(dbManager.slackUser().id, req.body); 87 | if (utils.isUrlEncodedForm(req) || utils.isMultipartForm(req)) { 88 | const channelId = utils.getChannelId(req.body.channel); 89 | res.redirect(`/messages/${channelId && channelId[0]}`); 90 | } else { 91 | const channel = utils.getChannelId(req.body.channel); 92 | const user = dbManager.slackUser(); 93 | const team = dbManager.slackTeam(); 94 | const response = createResponse({ 95 | user, 96 | team, 97 | ts: message.ts, 98 | channel, 99 | text: req.body.text.trim(), 100 | hideHeader: message.hideHeader 101 | }); 102 | broadcastResponse({ payload: response.message, userId: user.id, channel }); 103 | res.json(response); 104 | } 105 | } 106 | 107 | function rtmConnectHandler(req, res) { 108 | const token = (req.body && req.body.token) || req.headers.Authorization; 109 | const tokenHash = crypto 110 | .createHash('md5') 111 | .update(token) 112 | .digest('hex'); 113 | const successResponse = responses['rtm.connect']; 114 | 115 | const response = { 116 | ...successResponse 117 | }; 118 | const { id: userId, name: userName } = dbManager.slackUser(); 119 | response.self.id = userId; 120 | response.self.name = userName; 121 | 122 | const { id: teamId, domain, name: teamName } = dbManager.slackTeam(); 123 | response.team.id = teamId; 124 | response.team.domain = domain; 125 | response.team.name = teamName; 126 | const schema = process.env.URL_SCHEMA || 'http'; 127 | response.url = `${schema === 'http' ? 'ws' : 'wss'}://${domain}/ws?uid=${tokenHash}`; 128 | res.json(response); 129 | } 130 | 131 | function channelsListHandler(req, res) { 132 | const successResponse = { 133 | ...responses['channels.list'] 134 | }; 135 | 136 | successResponse.channels = dbManager.db.channels; 137 | res.json(successResponse); 138 | } 139 | 140 | function conversationsListHandler(req, res) { 141 | let { types } = req.query; 142 | const channelTypes = new Set(); 143 | if (!types) { 144 | types = ['public_channel', 'private_channel']; 145 | } 146 | 147 | if (Array.isArray(types)) { 148 | types.forEach(type => channelTypes.add(type)); 149 | } else { 150 | channelTypes.add(types); 151 | } 152 | 153 | res.json({ ok: true }); 154 | } 155 | 156 | function userInfoHandler(req, res) { 157 | let { user: userId } = req.query; 158 | const users = dbManager.users().findById(userId); 159 | if (!users.length) { 160 | res.json(responses.user_not_found); 161 | } else { 162 | const user = users[0]; 163 | const response = responses['users.info']; 164 | res.json({ 165 | ...response, 166 | user 167 | }); 168 | } 169 | } 170 | 171 | function chatUpdateHandler(req, res) { 172 | const { channel, ts, text } = req.body; 173 | const previousMessage = dbManager.channel(req.body.channel).findMessageByTs(ts); 174 | const user = dbManager.slackUser(); 175 | const team = dbManager.db.teams.filter(tm => tm.id === user.team_id)[0]; 176 | 177 | if (!previousMessage || previousMessage.user_id !== user.id || !text) { 178 | res.json(responses.cant_update_message); 179 | return; 180 | } 181 | 182 | const message = dbManager.channel(req.body.channel).updateMessage({ 183 | ...previousMessage, 184 | text 185 | }); 186 | 187 | const payload = factories.createUpdateMessageEvent({ 188 | channel, 189 | message, 190 | previousMessage 191 | }, { user, team }); 192 | 193 | broadcastResponse({ payload, userId: user.id, channel }); 194 | 195 | res.json({ 196 | ok: true, 197 | channel, 198 | ts, 199 | text 200 | }); 201 | } 202 | 203 | router.use('*', beforeAllHandler); 204 | 205 | router.post('/auth.test', authTestHandler); 206 | router.post('/chat.postMessage', postMessageHandler); 207 | router.post('/channels.list', channelsListHandler); 208 | router.post('/rtm.connect', rtmConnectHandler); 209 | router.post('/rtm.start', rtmConnectHandler); 210 | router.post('/chat.update', chatUpdateHandler); 211 | 212 | router.get('/rtm.connect', rtmConnectHandler); 213 | router.get('/rtm.start', rtmConnectHandler); 214 | router.get('/users.info', userInfoHandler); 215 | router.get('/conversations.list', conversationsListHandler); 216 | 217 | module.exports = router; 218 | -------------------------------------------------------------------------------- /routes/api/utils.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../../public/js/helpers'); 2 | 3 | function copyObject(obj) { 4 | return JSON.parse(JSON.stringify(obj)); 5 | } 6 | 7 | function getChannelId(channel) { 8 | const result = /^[CWDU][A-Z0-9]{8}$/.exec(channel); 9 | return (Array.isArray(result) && result[0]) || null; 10 | } 11 | 12 | function isOpenChannel(channel) { 13 | return channel && channel.startsWith('C'); 14 | } 15 | 16 | function isBot(channel, dbManager) { 17 | const users = dbManager.users().findById(channel, u => u.is_app_user || u.is_bot); 18 | return users.length; 19 | } 20 | 21 | function isUrlEncodedForm(req) { 22 | return req.headers['content-type'] === 'application/x-www-form-urlencoded'; 23 | } 24 | 25 | function isMultipartForm(req) { 26 | return req.headers['content-type'] && req.headers['content-type'].startsWith('multipart/form-data'); 27 | } 28 | 29 | module.exports = { 30 | copyObject, 31 | getChannelId, 32 | isOpenChannel, 33 | isBot, 34 | isUrlEncodedForm, 35 | isMultipartForm, 36 | createTs: helpers.createTs 37 | }; 38 | -------------------------------------------------------------------------------- /routes/app/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { dbManager } = require('../managers'); 4 | 5 | const constants = require('../constants'); 6 | 7 | router.get('/', (req, res) => { 8 | const selectedChannel = dbManager.db.channels.filter(ch => ch.name === 'general')[0]; 9 | const messages = dbManager.channel(selectedChannel.id).messages(constants.MESSAGES_MAX_COUNT); 10 | res.render(constants.MAIN_PAGE, { 11 | selectedChannel, 12 | selectedUser: null, 13 | selectedApp: null, 14 | channels: dbManager.db.channels, 15 | users: dbManager.db.users.filter(su => !su.is_bot && !su.is_app_user), 16 | bots: dbManager.db.users.filter(su => su.is_bot || su.is_app_user), 17 | team: dbManager.slackTeam(), 18 | me: dbManager.slackUser(), 19 | messages, 20 | constants 21 | }); 22 | }); 23 | 24 | router.get('/messages/:id', (req, res) => { 25 | const selectedChannel = dbManager.db.channels.filter(ch => ch.id === req.params.id); 26 | const selectedUser = dbManager.users().findById(req.params.id, u => !u.is_bot && !u.is_app_user); 27 | const selectedApp = dbManager.users().findById(req.params.id, u => u.is_bot || u.is_app_user); 28 | const messages = (selectedChannel.length && dbManager.channel(selectedChannel[0].id).messages(constants.MESSAGES_MAX_COUNT)) 29 | || (selectedUser.length && dbManager.channel(selectedUser[0].id).messages(constants.MESSAGES_MAX_COUNT)) 30 | || (selectedApp.length && dbManager.channel(selectedApp[0].id).messages(constants.MESSAGES_MAX_COUNT)) 31 | || []; 32 | res.render(constants.MAIN_PAGE, { 33 | selectedChannel: selectedChannel.length && selectedChannel[0], 34 | selectedUser: selectedUser.length && selectedUser[0], 35 | selectedApp: selectedApp.length && selectedApp[0], 36 | channels: dbManager.db.channels, 37 | users: dbManager.db.users.filter(su => !su.is_bot && !su.is_app_user), 38 | bots: dbManager.db.users.filter(su => su.is_bot || su.is_app_user), 39 | team: dbManager.slackTeam(), 40 | me: dbManager.slackUser(), 41 | messages, 42 | constants 43 | }); 44 | }); 45 | 46 | module.exports = router; 47 | -------------------------------------------------------------------------------- /routes/constants.js: -------------------------------------------------------------------------------- 1 | const WS_OPEN_STATE = 1; 2 | const MAIN_PAGE = process.env.MAIN_PAGE; 3 | const MESSAGES_MAX_COUNT = Number(process.env.MESSAGES_MAX_COUNT); 4 | const HIDE_HEADER_TIME_INTERVAL_IN_SECONDS = Number(process.env.DEFAULT_HEADER_HIDE_TIME_INTERVAL_IN_MIN) * 60; 5 | 6 | module.exports = { 7 | OPEN: WS_OPEN_STATE, 8 | MAIN_PAGE, 9 | MESSAGES_MAX_COUNT, 10 | HIDE_HEADER_TIME_INTERVAL_IN_SECONDS 11 | }; 12 | -------------------------------------------------------------------------------- /routes/managers.js: -------------------------------------------------------------------------------- 1 | const { createDbManager } = require('../db'); 2 | const { OPEN } = require('./constants'); 3 | 4 | class WsManager { 5 | constructor() { 6 | this.slackWss = new Set(); 7 | this.slackBots = new Set(); 8 | this.deleteActions = {}; 9 | 10 | this.deleteActions.bot = (client) => { 11 | this.slackBots.delete(client); 12 | }; 13 | this.deleteActions.ui = (client) => { 14 | this.slackWss.delete(client); 15 | }; 16 | } 17 | 18 | static sendByCondition(clients, msg, conditionFn = () => false) { 19 | clients.forEach(client => { 20 | if (client.readyState === OPEN && conditionFn(client)) { 21 | client.send(msg); 22 | } 23 | }); 24 | } 25 | 26 | sendJson(client, msg) { 27 | if (client.readyState === OPEN) { 28 | client.send(JSON.stringify(msg)); 29 | } else { 30 | this.deleteActions[client.clientType](client); 31 | } 32 | } 33 | 34 | broadcastToBot(msg, botId) { 35 | WsManager.sendByCondition(this.slackBots, msg, client => client.user.id === botId); 36 | } 37 | 38 | broadcast(msg, except) { 39 | WsManager.sendByCondition(this.slackWss, msg, client => client.user.id !== except); 40 | } 41 | 42 | broadcastToBots(msg, except) { 43 | WsManager.sendByCondition(this.slackBots, msg, client => client.user.id !== except); 44 | } 45 | } 46 | 47 | const dbManager = createDbManager(); 48 | const wsManager = new WsManager(); 49 | 50 | module.exports = { 51 | dbManager, 52 | wsManager 53 | }; 54 | -------------------------------------------------------------------------------- /routes/responses/auth.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "url": "PLACEHOLDER_WS_URL", 4 | "team": "PLACEHOLDER_TEAM_NAME", 5 | "user": "PLACEHOLDER_USER_NAME", 6 | "team_id": "PLACEHOLDER_TEAM_ID", 7 | "user_id": "PLACEHOLDER_USER_ID" 8 | } -------------------------------------------------------------------------------- /routes/responses/cant_update_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": false, 3 | "error": "cant_update_message" 4 | } -------------------------------------------------------------------------------- /routes/responses/channels.list.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "channels": [ 4 | { 5 | "id": "C0G9QF9GW", 6 | "name": "random", 7 | "is_channel": true, 8 | "created": 1449709280, 9 | "creator": "U0G9QF9C6", 10 | "is_archived": false, 11 | "is_general": false, 12 | "name_normalized": "random", 13 | "is_shared": false, 14 | "is_org_shared": false, 15 | "is_member": true, 16 | "is_private": false, 17 | "is_mpim": false, 18 | "members": [ 19 | "U0G9QF9C6", 20 | "U0G9WFXNZ" 21 | ], 22 | "topic": { 23 | "value": "Other stuff", 24 | "creator": "U0G9QF9C6", 25 | "last_set": 1449709352 26 | }, 27 | "purpose": { 28 | "value": "A place for non-work-related flimflam, faffing, hodge-podge or jibber-jabber you'd prefer to keep out of more focused work-related channels.", 29 | "creator": "", 30 | "last_set": 0 31 | }, 32 | "previous_names": [], 33 | "num_members": 2 34 | }, 35 | { 36 | "id": "C0G9QKBBL", 37 | "name": "general", 38 | "is_channel": true, 39 | "created": 1449709280, 40 | "creator": "U0G9QF9C6", 41 | "is_archived": false, 42 | "is_general": true, 43 | "name_normalized": "general", 44 | "is_shared": false, 45 | "is_org_shared": false, 46 | "is_member": true, 47 | "is_private": false, 48 | "is_mpim": false, 49 | "members": [ 50 | "U0G9QF9C6", 51 | "U0G9WFXNZ" 52 | ], 53 | "topic": { 54 | "value": "Talk about anything!", 55 | "creator": "U0G9QF9C6", 56 | "last_set": 1449709364 57 | }, 58 | "purpose": { 59 | "value": "To talk about anything!", 60 | "creator": "U0G9QF9C6", 61 | "last_set": 1449709334 62 | }, 63 | "previous_names": [], 64 | "num_members": 2 65 | } 66 | ], 67 | "response_metadata": { 68 | "next_cursor": "dGVhbTpDMUg5UkVTR0w=" 69 | } 70 | } -------------------------------------------------------------------------------- /routes/responses/chat.postMessage.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "channel": "[PLACEHOLDER]", 4 | "ts": "[PLACEHOLDER]", 5 | "message": { 6 | "client_msg_id": "[PLACEHOLDER]", 7 | "suppress_notification": false, 8 | "type": "message", 9 | "text": "[PLACEHOLDER]", 10 | "user": "[PLACEHOLDER]", 11 | "team": "[PLACEHOLDER]", 12 | "user_team": "[PLACEHOLDER]", 13 | "source_team": "[PLACEHOLDER]", 14 | "channel": "[PLACEHOLDER]", 15 | "event_ts": "[PLACEHOLDER]", 16 | "ts": "[PLACEHOLDER]" 17 | } 18 | } -------------------------------------------------------------------------------- /routes/responses/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const modules = {}; 4 | require('fs') 5 | .readdirSync(__dirname) 6 | .forEach((file) => { 7 | if (file.match(/\.json$/) !== null && file !== 'index.js') { 8 | const name = file.replace('.json', ''); 9 | modules[name] = JSON.parse(fs.readFileSync(path.join(__dirname, file))); 10 | } 11 | }); 12 | 13 | module.exports = modules; 14 | -------------------------------------------------------------------------------- /routes/responses/invalid_auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": false, 3 | "error": "invalid_auth" 4 | } -------------------------------------------------------------------------------- /routes/responses/rtm.connect.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "self": { 4 | "id": "W12345678", 5 | "name": "valera.petrov" 6 | }, 7 | "team": { 8 | "domain": "botfactory", 9 | "id": "T12345678", 10 | "name": "BotFactory" 11 | }, 12 | "url": "WS_CONNECTION_PLACEHOLDER" 13 | } -------------------------------------------------------------------------------- /routes/responses/team.info.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "team": { 4 | "id": "T12345678", 5 | "name": "BotFactory", 6 | "domain": "example", 7 | "email_domain": "example.com", 8 | "icon": { 9 | "image_34": "/images/slack_user.png", 10 | "image_44": "/images/slack_user.png", 11 | "image_68": "/images/slack_user.png", 12 | "image_88": "/images/slack_user.png", 13 | "image_102": "/images/slack_user.png", 14 | "image_132": "/images/slack_user.png", 15 | "image_default": true 16 | }, 17 | "enterprise_id": "E123456789", 18 | "enterprise_name": "Umbrella Corporation" 19 | } 20 | } -------------------------------------------------------------------------------- /routes/responses/user_not_found.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": false, 3 | "error": "user_not_found" 4 | } -------------------------------------------------------------------------------- /routes/responses/users.info.json: -------------------------------------------------------------------------------- 1 | { 2 | "ok": true, 3 | "user": {} 4 | } -------------------------------------------------------------------------------- /routes/rtm/index.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | const router = express.Router(); 4 | const { dbManager, wsManager } = require('../managers'); 5 | require('express-ws')(router); 6 | 7 | function setupMessageHandler(ws, handlers) { 8 | ws.on('message', (msg) => { 9 | const jsonMsg = JSON.parse(msg); 10 | if (handlers[jsonMsg.type]) { 11 | handlers[jsonMsg.type](ws, jsonMsg); 12 | } 13 | }); 14 | } 15 | 16 | const handlers = { 17 | ping: (ws, msg) => wsManager.sendJson(ws, { 18 | reply_to: msg.id, 19 | type: 'pong', 20 | time: msg.time 21 | }), 22 | message: (ws, msg) => { 23 | const response = { 24 | ok: true, 25 | channel: msg.channel, 26 | text: msg.text 27 | }; 28 | 29 | if (msg.id !== undefined) { 30 | response.reply_to = msg.id; 31 | } 32 | 33 | const message = dbManager.channel(msg.channel).createMessage(ws.user.id, msg); 34 | message.user = ws.user; 35 | message.team = ws.team; 36 | message.channel = msg.channel; 37 | response.message = message; 38 | response.ts = message.ts; 39 | response.channel = message.channel; 40 | 41 | const jsonPayload = JSON.stringify(response.message); 42 | 43 | wsManager.sendJson(ws, response); 44 | wsManager.broadcast(jsonPayload, ws.user.id); 45 | wsManager.broadcastToBots(jsonPayload, ws.user.id); 46 | }, 47 | typing: (ws, msg) => { 48 | handlers.user_typing(ws, msg); 49 | }, 50 | user_typing: (ws, msg) => { 51 | const message = JSON.stringify({ 52 | ...msg, 53 | channel: msg.channel.id || msg.channel, 54 | user: ws.user.id 55 | }); 56 | wsManager.broadcast(message, ws.user.id); 57 | wsManager.broadcastToBots(message, ws.user.id); 58 | } 59 | }; 60 | 61 | router.ws('/ws', (ws, req) => { 62 | /* eslint-disable no-param-reassign */ 63 | ws.clientType = 'bot'; 64 | const uid = req.query && req.query.uid; 65 | ws.user = dbManager.users().findById(dbManager.db.sessions[uid])[0]; 66 | ws.team = dbManager.teams().findById(ws.user.team_id)[0]; 67 | /* eslint-enable no-param-reassign */ 68 | 69 | wsManager.slackBots.add(ws); 70 | 71 | wsManager.sendJson(ws, { 72 | type: 'hello' 73 | }); 74 | 75 | setupMessageHandler(ws, handlers); 76 | 77 | ws.on('close', () => { 78 | wsManager.slackBots.delete(ws); 79 | }); 80 | }); 81 | 82 | router.ws('/slack', (ws) => { 83 | /* eslint-disable no-param-reassign */ 84 | ws.clientType = 'ui'; 85 | ws.user = dbManager.slackUser(); 86 | ws.team = dbManager.slackTeam(); 87 | /* eslint-enable no-param-reassign */ 88 | 89 | wsManager.slackWss.add(ws); 90 | 91 | setupMessageHandler(ws, handlers); 92 | 93 | ws.on('close', () => { 94 | wsManager.slackWss.delete(ws); 95 | }); 96 | }); 97 | 98 | module.exports = router; 99 | -------------------------------------------------------------------------------- /routes/testapi/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { dbManager } = require('../managers'); 4 | 5 | router.get('/reset', (req, res) => { 6 | dbManager.reset(); 7 | res.json({ ok: true }); 8 | }); 9 | 10 | router.get('/current/team', (req, res) => { 11 | if (typeof req.query.domain !== 'undefined') { 12 | dbManager.slackTeam().domain = req.query.domain; 13 | res.json({ 14 | ok: true 15 | }); 16 | } else { 17 | res.json({ 18 | ok: false 19 | }); 20 | } 21 | }); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /screenshots/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddevsio/mad-fake-slack/50548aada8cd98e2fedc5138560eaab96f0cee12/screenshots/.keep -------------------------------------------------------------------------------- /scripts/codeclimate/check.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const exec = util.promisify(require('child_process').exec); 3 | const expect = require('expect'); 4 | const fs = require('fs'); 5 | const chalk = require('chalk'); 6 | // eslint-disable-next-line no-console 7 | const log = console.log; 8 | const success = chalk.bold.green; 9 | const error = chalk.bold.red; 10 | const warning = chalk.keyword('orange'); 11 | 12 | async function isImagesInstalled() { 13 | const COMMAND = 'docker images | grep codeclimate/'; 14 | const codeClimateImages = [ 15 | 'codeclimate/codeclimate-structure', 16 | 'codeclimate/codeclimate-duplication', 17 | 'codeclimate/codeclimate' 18 | ]; 19 | const { stdout, stderr } = await exec(COMMAND); 20 | if (!stderr.length > 0) { 21 | const imageNames = stdout.split('\n').map(line => line.split(' ')[0].trim()).filter(Boolean); 22 | try { 23 | expect(imageNames).toStrictEqual(codeClimateImages); 24 | log(success('[+] All checks passed!')); 25 | } catch (e) { 26 | log(warning('You need to pull manually the following images:'), codeClimateImages); 27 | throw new Error('Needed images not installed manually'); 28 | } 29 | } else { 30 | log(error(stderr)); 31 | throw new Error(stderr.toString()); 32 | } 33 | } 34 | 35 | async function isInDocker() { 36 | const DOCKERENV = '/.dockerenv'; 37 | if (!fs.existsSync(DOCKERENV)) throw new Error('Not in Docker'); 38 | } 39 | 40 | isImagesInstalled() 41 | .then(isInDocker) 42 | .then(() => process.exit(0)) 43 | .catch((e) => { 44 | log(error(e)); 45 | process.exit(1); 46 | }); 47 | -------------------------------------------------------------------------------- /scripts/codeclimate/setup.sh: -------------------------------------------------------------------------------- 1 | cd /tmp \ 2 | && curl -L https://github.com/codeclimate/codeclimate/archive/master.tar.gz | tar xz \ 3 | && cd codeclimate-master \ 4 | && /bin/bash -l -c "make install" \ 5 | && /bin/bash -l -c "codeclimate engines:install" -------------------------------------------------------------------------------- /scripts/glitch.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const archiver = require('archiver'); 3 | const output = fs.createWriteStream(`glitch_release_${+new Date()}.zip`); 4 | const archive = archiver('zip', { 5 | zlib: { level: 9 } // Sets the compression level. 6 | }); 7 | 8 | const DomainHost = 'mad-fake-slack.glitch.me'; 9 | 10 | output.on('close', () => { 11 | // eslint-disable-next-line no-console 12 | console.log(`${archive.pointer()} total bytes`); 13 | // eslint-disable-next-line no-console 14 | console.log('archiver has been finalized and the output file descriptor has closed.'); 15 | }); 16 | 17 | output.on('end', () => { 18 | // eslint-disable-next-line no-console 19 | console.log('Data has been drained'); 20 | }); 21 | 22 | archive.on('warning', (err) => { 23 | if (err.code === 'ENOENT') { 24 | // log warning 25 | // eslint-disable-next-line no-console 26 | console.warn(err); 27 | } else { 28 | // throw error 29 | throw err; 30 | } 31 | }); 32 | 33 | archive.on('error', (err) => { 34 | throw err; 35 | }); 36 | 37 | archive.pipe(output); 38 | 39 | archive.directory('views/', 'views'); 40 | archive.glob('db/**/*', { ignore: ['db/teams.json'] }); 41 | archive.directory('public/', 'public'); 42 | archive.directory('routes/', 'routes'); 43 | archive.directory('config/', 'config'); 44 | archive.file('examples/rtmbot/index.js', { name: 'bot.js' }); 45 | archive.file('package-lock.json', { name: 'package-lock.json' }); 46 | archive.file('README.md', { name: 'README.md' }); 47 | archive.file('server.js', { name: 'server.js' }); 48 | archive.file('helpers.js', { name: 'helpers.js' }); 49 | 50 | const teams = JSON.parse(fs.readFileSync('./db/teams.json', 'utf8')); 51 | teams[0].domain = DomainHost; 52 | teams[0].email_domain = DomainHost; 53 | archive.append(JSON.stringify(teams, ' ', 2), { name: 'db/teams.json' }); 54 | 55 | const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); 56 | packageJson.scripts.start = `URL_SCHEMA=https ${packageJson.scripts.start}`; 57 | archive.append(JSON.stringify(packageJson, ' ', 2), { name: 'package.json' }); 58 | 59 | archive.finalize(); 60 | -------------------------------------------------------------------------------- /scripts/ntimes.sh: -------------------------------------------------------------------------------- 1 | for run in {1..10}; do npm run test:bdd && sleep 5; done -------------------------------------------------------------------------------- /scripts/volumes/inspect.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const exec = util.promisify(require('child_process').exec); 3 | const Promise = require('bluebird'); 4 | const MadFakeSlack = 'mad-fake-slack'; 5 | 6 | const DOCKER_VOLUME_LS = 'docker ps'; 7 | async function getRealPath() { 8 | const { stdout, stderr } = await exec(DOCKER_VOLUME_LS); 9 | if (stderr) throw new Error(stderr); 10 | 11 | const volumes = stdout.split('\n').map(line => line.split(' ').filter(Boolean)).filter(line => line.length).slice(1); 12 | const sources = await Promise.mapSeries(volumes, async ([id]) => { 13 | const { stdout: result } = await exec(`docker inspect -f "{{ json .Mounts }}" ${id}`); 14 | return result; 15 | }); 16 | const madFakeSlackData = sources.filter(line => line.indexOf('mad-fake-slack') !== -1); 17 | if (madFakeSlackData.length) { 18 | const foundVolumes = JSON.parse(madFakeSlackData[0]).filter(item => item.Source && item.Source.indexOf(MadFakeSlack) !== -1); 19 | if (foundVolumes.length) { 20 | return foundVolumes[0].Source; 21 | } 22 | } 23 | throw new Error('Real path to workspace not found'); 24 | } 25 | 26 | // eslint-disable-next-line no-console 27 | getRealPath().then(path => console.log(path)).catch(() => process.exit(1)); 28 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('./config/default')(); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const express = require('express'); 5 | const exphbs = require('express-handlebars'); 6 | const morgan = require('morgan'); 7 | const http = require('http'); 8 | const handlebarsHelpers = require('./helpers.js'); 9 | const apiRouter = require('./routes/api'); 10 | const appRouter = require('./routes/app'); 11 | const testApiRouter = require('./routes/testapi'); 12 | const rtmRouter = require('./routes/rtm'); 13 | const { spawn } = require('child_process'); 14 | 15 | const port = process.env.PORT; 16 | const host = process.env.HOST; 17 | 18 | /* eslint-disable-next-line */ 19 | const format = 20 | ':remote-addr - :remote-user [:date[clf]] ":method ' 21 | + ':url HTTP/:http-version" :type :status :res[content-length] ":referrer" ":user-agent"'; 22 | morgan.format('full', format); 23 | 24 | const app = express(); 25 | require('express-ws')(app); 26 | 27 | app.use(express.static('public')); 28 | app.use('/assets', express.static('node_modules')); 29 | app.engine('.hbs', exphbs({ extname: '.hbs', helpers: handlebarsHelpers })); 30 | app.set('view engine', '.hbs'); 31 | morgan.token('type', (req) => { 32 | return req.headers['content-type']; 33 | }); 34 | app.use( 35 | morgan('dev', { 36 | skip: (_, res) => { 37 | return res.statusCode < 400 || res.statusCode === 404; 38 | } 39 | }) 40 | ); 41 | app.use( 42 | morgan('full', { 43 | stream: fs.createWriteStream(path.join('/tmp', 'access.log'), { flags: 'a' }) 44 | }) 45 | ); 46 | app.use((req, res, next) => { 47 | const token = (req.body && req.body.token) || req.headers.Authorization; 48 | req.token = token; 49 | req.requestTime = Date.now(); 50 | next(); 51 | }); 52 | 53 | app.use('/', rtmRouter); 54 | app.use('/', appRouter); 55 | app.use('/api/db', testApiRouter); 56 | app.use('/api', apiRouter); 57 | 58 | function createUIServer({ httpPort, httpHost }) { 59 | const server = http.createServer(app); 60 | let serverPort = httpPort; 61 | let serverHost = httpHost; 62 | // eslint-disable-next-line global-require 63 | require('express-ws')(app, server); 64 | return { 65 | start(env) { 66 | if (env && typeof env === 'object') { 67 | process.env = { 68 | ...process.env, 69 | ...env 70 | }; 71 | serverPort = process.env.PORT; 72 | serverHost = process.env.HOST; 73 | } 74 | return new Promise(resolve => { 75 | server.listen(serverPort, serverHost, () => { 76 | resolve(server); 77 | }); 78 | }); 79 | }, 80 | close() { 81 | server.close(); 82 | } 83 | }; 84 | } 85 | 86 | if (require.main === module) { 87 | app.listen(port, host, () => { 88 | if (fs.existsSync('bot.js')) { 89 | spawn('npm', ['run', 'example:rtmbot:glitch']); 90 | } 91 | /* eslint-disable-next-line */ 92 | console.log(`Example app listening on port ${port}!`); 93 | }); 94 | } else { 95 | module.exports = createUIServer; 96 | } 97 | -------------------------------------------------------------------------------- /views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mad Fake Slack App 6 | 7 | {{> inlinestyles}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{{body}}} 15 | 16 | -------------------------------------------------------------------------------- /views/partials/appitem.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-inline-styles }} 2 | -------------------------------------------------------------------------------- /views/partials/appitemheader.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 |
5 | 8 |
9 | {{app.real_name}} 10 |

11 |
12 |
13 |
14 | 18 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /views/partials/channel.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-inline-styles }} 2 | -------------------------------------------------------------------------------- /views/partials/channelheader.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 | 15 |
16 |
17 | 21 |
22 |
23 | 27 |
28 |
29 | 30 | 31 | 32 | {{channel.topic.value}} 33 | 34 | 35 | 36 |
37 |
38 |
-------------------------------------------------------------------------------- /views/partials/directuser.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-bare-strings no-inline-styles }} 2 | -------------------------------------------------------------------------------- /views/partials/invite.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-inline-styles }} 2 |
3 |
4 |
5 |
6 | 7 |
8 |
Only visible to you
9 |
10 |
11 | 16 |
17 |
18 | 33 | 34 | You mentioned 35 | 36 | 37 | @Valera 38 | 39 | 40 | , but they’re not in this channel. 41 | 42 |
43 |
44 | 47 |
48 |
49 |
50 | 55 | 60 | 65 |
66 |
67 |
68 |
69 |
70 |
71 |
-------------------------------------------------------------------------------- /views/partials/mehref.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{me.real_name}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /views/partials/message.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-inline-styles }} 2 |
3 |
4 |
5 | {{#if (ne message.hideHeader true)}} 6 | 11 | {{/if}} 12 |
13 |
14 | {{#if (ne message.hideHeader true)}} 15 |
16 | 17 | 20 | {{message.user.real_name}} 21 | 22 | {{#if (or (eq message.user.is_bot true) (eq message.user.is_app_user true))}} 23 | 24 | 25 | APP 26 | 27 | 28 | {{/if}} 29 | 30 | 31 | {{toHumanTime message.ts}} 32 | 33 |
34 | {{/if}} 35 | {{! template-lint-disable block-indentation }} 36 | {{formatMessage message.text}} 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /views/partials/message_edit.hbs: -------------------------------------------------------------------------------- 1 | {{! template-lint-disable no-inline-styles }} 2 |
3 |
4 |
5 | 10 |
11 |
12 |
13 |
14 |
16 | {{! template-lint-disable block-indentation }} 17 | 20 |
21 | 25 |
26 | 27 |
28 |
29 | 36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /views/partials/statustyping.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{user.real_name}} is typing 3 |
-------------------------------------------------------------------------------- /views/partials/userchannelheader.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 20 |
21 |
22 | 23 | {{#if (eq user.id "USLACKBOT") }} 24 | 25 | {{else}} 26 | 27 | {{/if}} 28 | 29 | active 30 |
31 | {{#if (eq user.id "USLACKBOT") }} 32 |
33 | 34 | 35 | 36 | {{user.real_name}} 37 | 38 | 39 | 40 |
41 | {{/if}} 42 |
43 |
44 | --------------------------------------------------------------------------------