Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Implemented a mock slack api for testing #41

Open
wants to merge 4 commits into
base: feature/typescript-convert
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,32 @@ Many linting issues can be automatically fixed by running `yarn fix`.
## Local docker setup
This bot comes shipped with possibility to run it in docker.

ts-node-esm ./node_modules/.bin/knex migrate:make create_tables -x ts
benjamin-cizej marked this conversation as resolved.
Show resolved Hide resolved


1. Copy the example.env to .env
2. Fill the .env file
3. docker-compose up -d
4. (on the first time setup) ssh into nodejs container run `npm run db-migrate-latest` to prepare the database structure.
5. Visit http://${PROJECT_NAME}.localhost:4551/ to get the ngrok url
6. Paste the url into slack api Event subscriptions.

## Local testing slack api

It's possible to test the project without connecting to slack servers
- SLACK_API_TYPE=mock must be set in the .env file, also MOCK_API_PORT
- Slack credentials are also needed, but can be anything
- to run the mock server, ssh into the nodejs container and run `npm run mock_serve`

Talking to the mock slack api:
- possibly `chmod +x scontrol` in the project root
- ssh into nodejs container
- `./scontrol fromUser toUser channel ++` to send an event
- `./scontrol challenge` to send the challenge request

There are predefined users and channels already in the mock slack api:
- users: janez.kranjski, bobby, slackbot, agilekarma_bot
- channels: random, general

When sending an event using a user or a channel that don't exist yet in the api, they will be created,
however they will be lost when closing the mock server.
Same names can be used later, but they will have a different slackId and will thus be created anew in the
agilekarma database.
27 changes: 27 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as dotenv from 'dotenv';

dotenv.config();

const {
SLACK_BOT_USER_OAUTH_ACCESS_TOKEN: accessToken = '',
SCOREBOT_LEADERBOARD_URL: leaderboardUrl = '',
SCOREBOT_PORT: port = '80',
SCOREBOT_USE_SSL: useHttps = '0',
MOCK_SLACK_PORT: mockApiPort = '5010',
SLACK_API_TYPE: slackApiType,
SLACK_VERIFICATION_TOKEN: verificationToken = '',
SIGNING_SECRET: secret,
USER_LIMIT_VOTING_MAX: votingLimit = '300',
UNDO_TIME_LIMIT: timeLimit = '300',
DATABASE_HOST: databaseHost,
DATABASE_PORT: databasePort = '3306',
DATABASE_USER: databaseUser,
DATABASE_PASSWORD: databasePassword,
DATABASE_NAME: databaseName,
} = process.env;

export {
accessToken, leaderboardUrl, port, useHttps, mockApiPort, slackApiType,
verificationToken, secret, timeLimit, votingLimit, databaseHost, databasePort, databaseUser,
databasePassword, databaseName,
};
3 changes: 3 additions & 0 deletions docker/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ SCOREBOT_USE_SSL=1
SCOREBOT_LEADERBOARD_URL=localhost:3000
USER_LIMIT_VOTING_MAX=3
UNDO_TIME_LIMIT=300
# mock|default
SLACK_API_TYPE=
MOCK_SLACK_PORT=5010

# Database variables
DATABASE_NAME=
Expand Down
5 changes: 5 additions & 0 deletions src/environment.d.ts → environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ declare global {
SCOREBOT_LEADERBOARD_URL: string,
SCOREBOT_PORT: string
SCOREBOT_USE_SSL: string
SLACK_API_TYPE: 'real' | 'mock'
MOCK_SLACK_PORT: string
SLACK_VERIFICATION_TOKEN: string
USER_LIMIT_VOTING_MAX: string,
UNDO_TIME_LIMIT: string,
ENV: 'test' | 'dev' | 'prod';
}
}
Expand Down
22 changes: 9 additions & 13 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import slackClient from '@slack/client';
import { LogLevel, WebClient } from '@slack/web-api';
import bodyParser from 'body-parser';
import * as dotenv from 'dotenv';
import express from 'express';
import { handleGet, handlePost } from './src/app.js';
import { setSlackClient } from './src/slack.js';

dotenv.config();

const {
SLACK_BOT_USER_OAUTH_ACCESS_TOKEN: accessToken = '',
SCOREBOT_LEADERBOARD_URL: leaderboardUrl = '',
SCOREBOT_PORT: port = '80',
SCOREBOT_USE_SSL: useHttps = '0',
} = process.env;
import {
accessToken, leaderboardUrl, port, useHttps, mockApiPort, slackApiType,
} from './config.js';

const protocol = useHttps !== '1' ? 'http://' : 'https://';
const frontendUrl = protocol + leaderboardUrl;
const server = express();
// @ts-ignore
setSlackClient(new slackClient.WebClient(accessToken));

if (slackApiType === 'mock') {
setSlackClient(new WebClient(accessToken, { slackApiUrl: `http://localhost:${mockApiPort}/api/`, logLevel: LogLevel.DEBUG }));
} else {
setSlackClient(new WebClient(accessToken));
}
server.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', frontendUrl);
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
Expand Down
24 changes: 8 additions & 16 deletions knexfile.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import type { Knex } from 'knex';
import * as dotenv from 'dotenv';

dotenv.config();

const {
DATABASE_HOST: host,
DATABASE_PORT: port = '3306',
DATABASE_USER: user,
DATABASE_PASSWORD: password,
DATABASE_NAME: database,
} = process.env;
import {
databaseHost, databasePort, databaseUser, databasePassword, databaseName,
} from 'config.js';

const config: Knex.Config = {
client: 'mysql',
connection: {
host,
port: Number.parseInt(port, 10),
user,
password,
database,
host: databaseHost,
port: Number.parseInt(databasePort, 10),
user: databaseUser,
password: databasePassword,
database: databaseName,
},
migrations: {
tableName: 'knex_migrations',
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
"dev": "nodemon",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"test": "jest --forceExit --runInBand",
"mock_serve": "ts-node-esm src/mock_slack_api/slack_server.ts",
"db-migrate-up": "ts-node-esm ./node_modules/.bin/knex migrate:up",
"db-migrate-down": "ts-node-esm ./node_modules/.bin/knex migrate:down",
"db-migrate-latest": "ts-node-esm ./node_modules/.bin/knex migrate:latest",
"db-migrate-rollback-all": "ts-node-esm ./node_modules/.bin/knex migrate:rollback all",
"unit-tests": "SKIP_INTEGRATION_TESTS=true SKIP_E2E_TESTS=true jest --forceExit",
"integration-tests": "SKIP_E2E_TESTS=true jest --forceExit integration-tests",
"e2e-tests": "SKIP_INTEGRATION_TESTS=true jest --forceExit e2e-tests",
Expand All @@ -33,6 +35,7 @@
"dependencies": {
"@slack/client": "^5.0.2",
"@slack/web-api": "^6.8.0",
"axios": "^1.2.1",
"body-parser": "^1.18.3",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
Expand Down
29 changes: 29 additions & 0 deletions scontrol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/sh


challenge() {
curl -X POST http://localhost:"$MOCK_SLACK_PORT"/api/control.challenge -w "\n"
}

message() {
fromUser=$1
toUser=$2
channel=$3
message=$4

curl -X POST http://localhost:"$MOCK_SLACK_PORT"/api/control.message -w "\n" -H 'Content-Type: application/json' -d "{\"fromUser\": \"$fromUser\", \"toUser\": \"$toUser\", \"channel\": \"$channel\", \"message\": \"$message\"}"
}

if [ ${#} -eq 1 ] && [ "${1}" = "challenge" ]
then
challenge

elif [ ${#} -eq 4 ]
then
message "${1}" "${2}" "${3}" "${4}"
else
printf "Wrong arguments\n"
exit 1 # wrong args
fi


8 changes: 2 additions & 6 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import {
getKarmaFeed,
getUserProfile,
} from './leaderboard.js';
import knexInstance from './database/knex.js';

const {
SLACK_VERIFICATION_TOKEN: verificationToken = '',
} = process.env;
import { verificationToken } from '../config.js';

export const logRequest = (request: Request) => {
console.log(`${request.ip} ${request.method} ${request.path} ${request.headers['user-agent']}`);
Expand Down Expand Up @@ -87,7 +83,7 @@ export const handlePost: RequestHandler = (request, response) => {

// Respond to challenge sent by Slack during event subscription set up.
if (request.body.challenge) {
response.send({challenge: request.body.challenge});
response.send({ challenge: request.body.challenge });
console.info('200 Challenge response sent');
return;
}
Expand Down
24 changes: 8 additions & 16 deletions src/database/knex.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import type { Knex } from 'knex';
import * as knexPkg from 'knex';
import * as dotenv from 'dotenv';

dotenv.config();

const {
DATABASE_HOST: host,
DATABASE_PORT: port = '3306',
DATABASE_USER: user,
DATABASE_PASSWORD: password,
DATABASE_NAME: database,
} = process.env;
import {
databaseHost, databasePort, databaseUser, databasePassword, databaseName,
} from '../../config.js';

const config: Knex.Config = {
client: 'mysql',
connection: {
host,
port: Number.parseInt(port, 10),
user,
password,
database,
host: databaseHost,
port: Number.parseInt(databasePort, 10),
user: databaseUser,
password: databasePassword,
database: databaseName,
},
};

Expand Down
10 changes: 3 additions & 7 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@ import crypto from 'crypto';
import Handlebars from 'handlebars';
import type { Nullable, PlusMinusEventData } from './types.js';
import { getUserName } from './slack.js';
import { secret, verificationToken } from '../config.js';

const templates: Record<string, string> = {};

const {
SLACK_VERIFICATION_TOKEN: slackToken,
SIGNING_SECRET: secret,
} = process.env;

const ONE_DAY = 60 * 60 * 24;
const TOKEN_TTL = ONE_DAY;
const MILLISECONDS_TO_SECONDS = 1000;
Expand Down Expand Up @@ -78,12 +74,12 @@ export const getTimeBasedToken = (ts: string): string => {
throw Error('Timestamp not provided when getting time-based token.');
}

if (!slackToken || !secret) {
if (!verificationToken || !secret) {
throw Error('SLACK_VERIFICATION_TOKEN or SIGNING_SECRET env variable missing.');
}

return crypto
.createHmac('sha256', slackToken)
.createHmac('sha256', verificationToken)
.update(ts + secret)
.digest('hex');
};
Expand Down
13 changes: 6 additions & 7 deletions src/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { getChannelName, getUserName, sendEphemeral } from './slack.js';
import {
getAllChannels,
getName,
getUserId,
retrieveTopScores,
getAll,
getAllScoresFromUser as getAllScoresFromUserPoints,
getKarmaFeed as getKarmaFeedPoints,
} from './points.js';
import { logResponseError } from './app.js';
import { useHttps, leaderboardUrl } from '../config.js';

/**
* Gets the URL for the full leaderboard, including a token to ensure that it is only viewed by
Expand All @@ -31,17 +31,16 @@ export const getLeaderboardUrl = (request: Request, channelId: string): string =
const hostname = request.headers.host;

const params = { channel: channelId };
const protocol = process.env.SCOREBOT_USE_SSL !== '1' ? 'http://' : 'https://';
const protocol = useHttps !== '1' ? 'http://' : 'https://';

return `${protocol}${hostname}/leaderboard?${querystring.stringify(params)}`;
};

const getLeaderboardWeb = (request: Request, channelId: string): string => {
const params = { channel: channelId };
const protocol = process.env.SCOREBOT_USE_SSL !== '1' ? 'http://' : 'https://';
const frontendUrl = process.env.SCOREBOT_LEADERBOARD_URL;
const protocol = useHttps !== '1' ? 'http://' : 'https://';

return `${protocol}${frontendUrl}?${querystring.stringify(params)}`;
return `${protocol}${leaderboardUrl}?${querystring.stringify(params)}`;
};

/**
Expand Down Expand Up @@ -264,11 +263,11 @@ export const getUserProfile = async (request: Request): Promise<any> => {
channelProfile: channel,
} = request.query as Record<string, string>;
const scores = await retrieveTopScores(channel);
const [users, userId] = await Promise.all([rankItems(scores, 'users', 'object'), getUserId(username)]);
const users = await rankItems(scores, 'users', 'object');

let userRank = 0;
for (const el of users) {
if (el.item_id === userId) {
if (el.item_id === username) {
userRank = el.rank;
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/mock_slack_api/mock_data/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const {
SLACK_VERIFICATION_TOKEN: token,
} = process.env;

const workspaceData = {
context_team_id: 'T04DD0FC16Z',
token,
api_app_id: 'A04DFUS9W58',
};

export default workspaceData;
Loading