Skip to content

Commit

Permalink
lib: [WIP] Convert slackMock.js to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
hakatashi committed Aug 1, 2023
1 parent 4c4b817 commit b1a261f
Show file tree
Hide file tree
Showing 38 changed files with 144 additions and 120 deletions.
5 changes: 2 additions & 3 deletions achievement-quiz/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import achievementQuiz from './index';
import Slack from '../lib/slackMock';

const Slack = require('../lib/slackMock.js');

let slack: typeof Slack;
let slack: Slack;

beforeEach(() => {
slack = new Slack();
Expand Down
3 changes: 1 addition & 2 deletions achievements/index_production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import noop from 'lodash/noop';
// @ts-expect-error
import MockFirebase from 'mock-cloud-firestore';
// @ts-expect-error
import Slack from '../lib/slackMock.js';
import Slack from '../lib/slackMock';

import achievements from './index_production';

Expand Down
2 changes: 1 addition & 1 deletion ahokusa/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
jest.mock('../achievements');

const ahokusa = require('./index.js');
const Slack = require('../lib/slackMock.js');
const {default: Slack} = require('../lib/slackMock.ts');

let slack = null;

Expand Down
4 changes: 2 additions & 2 deletions atequiz/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WebAPICallOptions, WebClient } from '@slack/web-api';
import type { TeamEventClient } from '../lib/slackEventClient';
import type { TeamEventClientInterface } from '../lib/slackEventClient';
import { SlackInterface } from '../lib/slack';
import { ChatPostMessageArguments } from '@slack/web-api/dist/methods';
import assert from 'assert';
Expand Down Expand Up @@ -52,7 +52,7 @@ export const typicalMessageTextsGenerator = {
* To use other judge/watSecGen/ngReaction, please extend this class.
*/
export class AteQuiz {
eventClient: TeamEventClient;
eventClient: TeamEventClientInterface;
slack: WebClient;
problem: AteQuizProblem;
ngReaction: string | null = 'no_good';
Expand Down
2 changes: 1 addition & 1 deletion channel-notifier/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

jest.mock('axios');

const Slack = require('../lib/slackMock.js');
const {default: Slack} = require('../lib/slackMock.ts');
const channelNotifier = require('./index.js');

let slack = null;
Expand Down
2 changes: 1 addition & 1 deletion checkin/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
jest.mock('axios');

const axios = require('axios');
const Slack = require('../lib/slackMock.js');
const {default: Slack} = require('../lib/slackMock.ts');
const checkin = require('./index.js');

let slack = null;
Expand Down
2 changes: 1 addition & 1 deletion dajare/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jest.mock('./tokenize.js');
jest.mock('../achievements');

const dajare = require('./index.js');
const Slack = require('../lib/slackMock.js');
const {default: Slack} = require('../lib/slackMock.ts');
const tokenize = require('./tokenize.js');

tokenize.virtualTokens = {
Expand Down
5 changes: 5 additions & 0 deletions discord/hayaoshiUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import {inspect} from 'util';
import {extractValidAnswers} from './hayaoshiUtils';

jest.mock('../hayaoshi', () => ({
isCorrectAnswer: jest.fn(),
normalize: jest.fn(),
}));

const testCases: [string, string, string[]][] = [
['', 'リトグラフ[lithograph]【「石版画」「石版印刷」「リトグラフィー[lithographie]」も○】',
['リトグラフ', 'lithograph', '石版画', '石版印刷', 'リトグラフィー', 'lithographie'],
Expand Down
2 changes: 1 addition & 1 deletion emoji-notifier/index.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-env node, jest */

const Slack = require('../lib/slackMock.js');
const {default: Slack} = require('../lib/slackMock.ts');
const emojiNotifier = require('./index.js');

let slack = null;
Expand Down
3 changes: 1 addition & 2 deletions hitandblow/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { stripIndent } from 'common-tags';
import hitandblow from './';

const Slack = require('../lib/slackMock.js');
import Slack from '../lib/slackMock';

let slack: typeof Slack;

Expand Down
4 changes: 2 additions & 2 deletions lib/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import sql from 'sql-template-strings';
import * as sqlite from 'sqlite';
import sqlite3 from 'sqlite3';
import path from 'path';
import {TeamEventClient} from './slackEventClient';
import {TeamEventClient, TeamEventClientInterface} from './slackEventClient';
import {Deferred} from './utils';
import {Token} from '../oauth/tokens';

export interface SlackInterface {
webClient: WebClient;
eventClient: TeamEventClient;
eventClient: TeamEventClientInterface;
messageClient: ReturnType<typeof createMessageAdapter>;
};

Expand Down
1 change: 0 additions & 1 deletion lib/slackCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
EmojiListArguments,
EmojiListResponse,
} from '@slack/web-api';
// @ts-expect-error
import Slack from './slackMock';
import SlackCache from './slackCache';

Expand Down
5 changes: 2 additions & 3 deletions lib/slackCache.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {get} from 'lodash';
import type {Token} from '../oauth/tokens';
import {Deferred} from './utils';
import {TeamEventClient} from './slackEventClient';
import {TeamEventClient, TeamEventClientInterface} from './slackEventClient';
import logger from './logger';

import type {SlackEventAdapter} from '@slack/events-api';
import type {Reaction} from '@slack/web-api/dist/response/ConversationsHistoryResponse';
import type {Member} from '@slack/web-api/dist/response/UsersListResponse';
import type {
Expand Down Expand Up @@ -32,7 +31,7 @@ interface WebClient {

interface Config {
token: Token;
eventClient: SlackEventAdapter;
eventClient: TeamEventClientInterface;
webClient: WebClient;
enableReactions?: boolean;
}
Expand Down
17 changes: 10 additions & 7 deletions lib/slackEventClient.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
import type {SlackEventAdapter} from '@slack/events-api';

export class TeamEventClient {
private readonly eventAdapter: SlackEventAdapter;
private readonly team: string;
#eventAdapter: SlackEventAdapter;
#team: string;

// contract: 渡されるeventAdapterは、EventAdapterOptions.includeBodyがtrueでなければならない。
constructor(eventAdapter: SlackEventAdapter, team: string) {
this.eventAdapter = eventAdapter;
this.team = team;
this.#eventAdapter = eventAdapter;
this.#team = team;
}

// listen on events against all teams.
onAllTeam(event: string, listener: (...args: any[]) => void): any {
return this.eventAdapter.on(event, listener);
return this.#eventAdapter.on(event, listener);
}
// listen on events against the team.
on(event: string, listener: (...args: any[]) => void): any {
return this.eventAdapter.on(event, (...args: any[]) => {
return this.#eventAdapter.on(event, (...args: any[]) => {
// https://slack.dev/node-slack-sdk/events-api#receive-additional-event-data
// https://github.com/slackapi/node-slack-sdk/blob/3e9c483c593d6aa28f6f5680f287722df3327609/packages/events-api/src/http-handler.ts#L212-L223
// https://api.slack.com/apis/connections/events-api#the-events-api__receiving-events__events-dispatched-as-json
// args: [body.event, body: {team_id: string}]
if (args[1].team_id === this.team) {
if (args[1].team_id === this.#team) {
listener(...args);
}
});
}

// feel free to add any other [Events](https://nodejs.org/api/events.html) methods you want!
}

// https://stackoverflow.com/a/48953930
export type TeamEventClientInterface = Pick<TeamEventClient, keyof TeamEventClient>;
90 changes: 60 additions & 30 deletions lib/slackMock.js → lib/slackMock.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* eslint-env node, jest */

const EventEmitter = require('events');
const noop = require('lodash/noop');
const last = require('lodash/last');
import type {ChatPostMessageArguments, ChatPostMessageResponse, ReactionsAddArguments, ReactionsAddResponse, WebClient} from '@slack/web-api';
import {EventEmitter} from 'events';
import {noop, last} from 'lodash';
import type {SlackInterface} from './slack';
import { TeamEventClient, TeamEventClientInterface } from './slackEventClient';

// https://jestjs.io/docs/mock-function-api
const mockMethodCalls = [
Expand All @@ -19,20 +21,33 @@ const mockMethodCalls = [
'mockClear',
'mockReset',
'mockName',
];
] as const;

const createWebClient = (fallbackFn, registeredMocks) => {
const handler = (stack) => {
const isMockMethodCall = (name: string): name is (typeof mockMethodCalls)[number] => (
(mockMethodCalls as readonly string[]).includes(name)
);

interface MockWebClient extends Record<string, MockWebClient> {
(...args: any[]): any;
}

const createWebClient = (
fallbackFn: (stack: string[], ...args: any[]) => Promise<any>,
registeredMocks: Map<string, jest.Mock>,
) => {
const handler = (stack: string[]): MockWebClient => {
return new Proxy(
(...args) => {
(...args: any[]) => {
const path = stack.join('.');
const methodName = last(stack);
if (registeredMocks.has(path)) {
return registeredMocks.get(path)(...args);
}
if (mockMethodCalls.includes(last(stack))) {
if (isMockMethodCall(methodName)) {
const mock = jest.fn();
registeredMocks.set(stack.slice(0, -1).join('.'), mock);
return mock[last(stack)](...args);
const mockArgs = args as Parameters<typeof mock['mockImplementation']>;
return mock[methodName](...mockArgs);
}
return fallbackFn(stack, ...args)
},
Expand All @@ -48,66 +63,82 @@ const createWebClient = (fallbackFn, registeredMocks) => {
return Reflect.get(name, property, receiver);
},
}
);
) as MockWebClient;
};

return handler([]);
};

module.exports = class SlackMock extends EventEmitter {
constructor(...args) {
super(...args);
this.fakeChannel = 'C00000000';
this.fakeUser = 'U00000000';
this.fakeTimestamp = '1234567890.123456';
this.eventClient = new EventEmitter();
class MockTeamEventClient extends EventEmitter {
onAllTeam(event: string, listener: (...args: any[]) => void): any {
return this.on(event, listener);
}
}

export default class SlackMock extends EventEmitter implements SlackInterface {
fakeChannel = 'C00000000';
fakeUser = 'U00000000';
fakeTeam = 'T00000000';
fakeTimestamp = '1234567890.123456';

readonly eventClient: TeamEventClientInterface & EventEmitter;
readonly registeredMocks: Map<string, jest.Mock>;
readonly webClient: WebClient;
readonly messageClient: any;

constructor() {
super();
this.eventClient = new MockTeamEventClient();
this.registeredMocks = new Map();
this.webClient = createWebClient((...args) => this.handleWebcall(...args), this.registeredMocks);
this.webClient = createWebClient(
(stack: string[], ...args: any[]) => this.handleWebcall(stack, ...args),
this.registeredMocks,
) as unknown as WebClient;
this.messageClient = {
action: noop,
viewSubmission: noop,
};
}

handleWebcall(stack, ...args) {
handleWebcall(stack: string[], ...args: any[]) {
this.emit('webcall', stack, ...args);
this.emit(stack.join('.'), ...args);
if (stack.join('.') === "emoji.list") {
return Promise.resolve({ok: true, emoji: {"fakeemoji": "https://example.com"}});
}
if (stack.join('.') === "users.list") {
return {members: []}
return Promise.resolve({ok: true, members: []});
}
if (stack.join('.') === "conversations.list") {
return Promise.resolve({ok: true, channels: [
{id: 'CGENERAL', is_general: true},
]});
}
if (stack.join('.') === "chat.postMessage") {
if (stack.join('.') === "chat.postMessage") {
return Promise.resolve({ok: true, ts: this.fakeTimestamp});
}
if (stack.join('.') === "chat.unfurl") {
if (stack.join('.') === "chat.unfurl") {
return Promise.resolve({ok: true});
}
// TODO: make returned value customizable
return Promise.resolve([]);
}

postMessage(message, options={}) {
postMessage(message: string, options = {}) {
const data = {
channel: this.fakeChannel,
text: message,
user: this.fakeUser,
ts: this.fakeTimestamp,
type: "message",
type: 'message',
...options
};
this.eventClient.emit('message', data);
}

waitForEvent(eventName){
waitForEvent(eventName: string) {
return new Promise((resolve) => {
const handleResponse = (options) => {
const handleResponse = (options: {channel: string} & Record<string, any>) => {
if (options.channel === this.fakeChannel) {
this.removeListener(eventName, handleResponse);
resolve(options);
Expand All @@ -119,22 +150,21 @@ module.exports = class SlackMock extends EventEmitter {
}

waitForResponse() {
return this.waitForEvent('chat.postMessage');
return this.waitForEvent('chat.postMessage') as Promise<ChatPostMessageArguments>;
}

waitForReaction() {
return this.waitForEvent('reactions.add');
return this.waitForEvent('reactions.add') as Promise<ReactionsAddArguments>;
}

getResponseTo(message, options={}) {
getResponseTo(message: string, options = {}) {
const res = this.waitForResponse();
this.postMessage(message, options);
return res;
}

// Not recommended. Instanciate a new SlackMock instead.
reset() {
this.eventClient.removeAllListeners();
this.removeAllListeners();
this.registeredMocks.clear();
}
Expand Down
2 changes: 1 addition & 1 deletion lib/slackUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const mrkdwn = (text: string): MrkdwnElement => ({

const isGenericMessage = (message: MessageEvent): message is GenericMessageEvent => (
message.subtype === undefined
)
);

export const extractMessage = (message: MessageEvent) => {
if (isGenericMessage(message)) {
Expand Down
3 changes: 1 addition & 2 deletions lyrics/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ jest.mock('tinyreq');
jest.mock('axios');

import lyrics from './index';
// @ts-expect-error
import Slack from '../lib/slackMock.js';
import Slack from '../lib/slackMock';
// @ts-expect-error
import tinyreq from 'tinyreq';
import axios from 'axios';
Expand Down
2 changes: 1 addition & 1 deletion mahjong/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

jest.mock('../achievements');

const Slack = require('../lib/slackMock.js');
const {default: Slack} = require('../lib/slackMock.ts');
const mahjong = require('./index.js');

let slack = null;
Expand Down
Loading

0 comments on commit b1a261f

Please sign in to comment.