From b89de0e63cb43df991d5540726509ddf400b1d38 Mon Sep 17 00:00:00 2001 From: Georgii Bagretsov Date: Thu, 12 May 2022 15:07:20 +0300 Subject: [PATCH] =?UTF-8?q?#123=20=D0=9C=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20G?= =?UTF-8?q?ame=20=D0=B8=20=D0=B5=D0=B3=D0=BE=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20TypeScript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jest.config.js | 1 + app/src/db.ts | 2 + app/src/game/admin.js | 21 - app/src/game/admin.ts | 24 ++ app/src/game/game.js | 294 -------------- app/src/game/game.test.js | 366 ------------------ app/src/game/game.test.ts | 270 +++++++++++++ app/src/game/game.ts | 276 +++++++++++++ app/src/game/model/AddWordResult.ts | 5 + .../game/test-resources/google-responses.ts | 14 + app/src/game/test-resources/messages.ts | 35 ++ package-lock.json | 16 +- package.json | 3 +- 13 files changed, 629 insertions(+), 698 deletions(-) delete mode 100644 app/src/game/admin.js create mode 100644 app/src/game/admin.ts delete mode 100644 app/src/game/game.js delete mode 100644 app/src/game/game.test.js create mode 100644 app/src/game/game.test.ts create mode 100644 app/src/game/game.ts create mode 100644 app/src/game/model/AddWordResult.ts create mode 100644 app/src/game/test-resources/google-responses.ts create mode 100644 app/src/game/test-resources/messages.ts diff --git a/app/jest.config.js b/app/jest.config.js index 3ee6e67..d30664a 100644 --- a/app/jest.config.js +++ b/app/jest.config.js @@ -12,5 +12,6 @@ module.exports = { '**/daily.test.ts', '**/holidays.test.ts', '**/statistics.test.ts', + '**/game.test.ts', ], // TODO: add all test files when they are fixed }; diff --git a/app/src/db.ts b/app/src/db.ts index 4bcd405..8cc1018 100644 --- a/app/src/db.ts +++ b/app/src/db.ts @@ -2,6 +2,8 @@ import {Client, QueryResult, types} from 'pg'; types.setTypeParser(types.builtins.INT8, val => parseInt(val, 10)); +export const DUPLICATE_KEY_PG_ERROR = '23505'; + async function getClient(): Promise { const client = new Client({ connectionString: process.env.DATABASE_URL, diff --git a/app/src/game/admin.js b/app/src/game/admin.js deleted file mode 100644 index 359dfb0..0000000 --- a/app/src/game/admin.js +++ /dev/null @@ -1,21 +0,0 @@ -const db = require('../db'); - -async function addWord(word, approved = true) { - try { - return await db.query(`INSERT INTO friends_vk_bot.words (name, approved) VALUES ('${ word }', ${ approved });`); - } catch (error) { - console.log(error.detail); - return error.code; - } -} - -async function deleteWord(word) { - try { - return await db.query(`DELETE FROM friends_vk_bot.words WHERE name = '${ word }';`); - } catch (error) { - console.log(error); - } -} - -module.exports.addWord = addWord; -module.exports.deleteWord = deleteWord; diff --git a/app/src/game/admin.ts b/app/src/game/admin.ts new file mode 100644 index 0000000..c56a37c --- /dev/null +++ b/app/src/game/admin.ts @@ -0,0 +1,24 @@ +import db, {DUPLICATE_KEY_PG_ERROR} from '../db'; +import {AddWordResult} from './model/AddWordResult'; + +async function addWord(word: string, approved: boolean): Promise { + try { + await db.query(`INSERT INTO friends_vk_bot.words (name, approved) VALUES ('${ word }', ${ approved });`); + return AddWordResult.SUCCESS; + } catch (error) { + return (error as any).code === DUPLICATE_KEY_PG_ERROR ? AddWordResult.DUPLICATE_WORD : AddWordResult.ERROR; + } +} + +async function deleteWord(word: string): Promise { + try { + await db.query(`DELETE FROM friends_vk_bot.words WHERE name = '${ word }';`); + } catch (error) { + console.log(error); + } +} + +export default { + deleteWord, + addWord, +}; diff --git a/app/src/game/game.js b/app/src/game/game.js deleted file mode 100644 index b9ad0ad..0000000 --- a/app/src/game/game.js +++ /dev/null @@ -1,294 +0,0 @@ -const needle = require('needle'); -require('dotenv').config(); -const uuid = require('uuid'); - -const vk = require('../vk/vk'); -const dbClient = require('../db'); -const admin = require('./admin'); -const { getPluralForm } = require('../util'); - -const TABLE_WORDS = 'words'; - -const STEP_INTERVAL = process.env.GAME_STEP_INTERVAL || 15000; - -let message = null; -let answer = ''; -let isPlaying = false; -let gameId = ''; -let timeoutObj = null; -let taskImgBuffer = null; -let hintImgBuffer = null; -let lettersHintSent = false; - -async function getRandomTask() { - // TODO: поддержка категорий - let query = ` - SELECT name FROM friends_vk_bot.${TABLE_WORDS} WHERE approved = true - ORDER BY RANDOM() LIMIT 1; - `; - - const dbResult = await dbClient.query(query); - return { - answer: dbResult.rows[0].name, - }; -} - -function handleMessage() { - if (!message.text) { - return false; - } - if (isAddWordRequest(message.text)) { - handleAddWordRequest(message.text); - return true; - } - if (isDeleteWordRequest(message.text)) { - handleDeleteWordRequest(message.text); - return true; - } - if (isPlaying) { - return handlePlayingState(); - } - if (isGameRequestMessage(message.text)) { - handleGameRequestMessage(message.text); - return true; - } - return false; -} - -function isAddWordRequest(text) { - const botMentioned = isBotMentioned(text); - const containsAddWordRequest = text.includes('запомни слово'); - return botMentioned && containsAddWordRequest; -} - -async function handleAddWordRequest(text) { - if (isAddWordRequest(text)) { - let word = extractWord(text); - - if (word) { - const result = await admin.addWord(word); - if (result === '23505') { - vk.sendMessage(`Я уже знаю слово "${ word }"! 😊`, 3000); - } else { - vk.sendMessage(`👍 Я запомнил слово "${ word }"!`, 3000); - } - } else { - vk.getUserName(message.from_id) - .then(name => vk.sendMessage(`${name}, я тебя не понимаю 😒`, 3000)); - } - } -} - -function isDeleteWordRequest(text) { - const botMentioned = isBotMentioned(text); - const containsDeleteWordRequest = text.includes('забудь слово'); - return botMentioned && containsDeleteWordRequest; -} - -async function handleDeleteWordRequest(text) { - if (isDeleteWordRequest(text)) { - let word = extractWord(text); - - if (word) { - await admin.deleteWord(word); - vk.sendMessage(`👍 Я забыл слово "${ word }"!`, 3000); - } else { - vk.getUserName(message.from_id) - .then(name => vk.sendMessage(`${name}, я тебя не понимаю 😒`, 3000)); - } - } -} - -function extractWord(text) { - let parts = text.split(' '); - let word = parts[parts.length - 1]; - - word = word.replace(/[.,/#!$%^&*;:{}=\-_`~()a-zA-Z\d]/g, ''); - - return word === '' ? false : word; -} - -async function handleGameRequestMessage(text) { - - if (isGameRequestMessage(text)) { - isPlaying = true; - gameId = uuid.v4(); - const task = await getRandomTask(); - try { - await generatePhotos(task.answer); - // TODO: больше приветственных сообщений - let welcomeMessages = [ - 'Игра начинается, отгадывать могут все! 😏 Какое слово я загадал?', - 'Я люблю играть! 😊 Я загадал слово, которое описывает эту картинку. Сможете угадать это слово?', - ]; - await vk.sendMessage(welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)], 3000); - await vk.sendPhotoToChat(taskImgBuffer); - answer = task.answer; - if (Math.random() > 0.5) { - await vk.sendMessage(getLettersHintMessage()); - lettersHintSent = true; - } - console.log(`Correct answer: ${answer}`); - timeoutObj = setTimeout(() => giveHint(gameId), STEP_INTERVAL); - } catch (error) { - resetGame(); - // Бот устал - if (error.message === 'usageLimits') { - // TODO: больше сообщений - let limitsMessages = [ - 'Что-то я устал играть... 😫 Приходите завтра 😊', - 'Давай продолжим завтра? Сегодня больше не хочется 😳', - 'Я уже наигрался, мне надо отдохнуть', - ]; - - let limitsStickers = [13, 85, 2091, 5135, 5629]; - - await vk.sendSticker(limitsStickers[Math.floor(Math.random() * limitsStickers.length)]); - await vk.sendMessage(limitsMessages[Math.floor(Math.random() * limitsMessages.length)], 5000); - } - } - } -} - -async function generatePhotos(answer) { - let apiURL = 'https://www.googleapis.com/customsearch/v1'; - let key = process.env.GOOGLE_KEY; - let cx = process.env.GOOGLE_SEARCH_ENGINE_ID; - let start = randomInteger(1, 5); - - let url = `${apiURL}?q=${encodeURIComponent(answer)}&cx=${cx}&fileType=jpg&num=2&safe=active&searchType=image&fields=items%2Flink&start=${start}&key=${key}`; - const googleResponse = await needle('get', url); - console.log(googleResponse.body); - if (googleResponse.body.error && googleResponse.body.error.errors[0].domain === 'usageLimits') { - throw new Error('usageLimits'); - } - const taskImgURL = googleResponse.body.items[0].link; - const hintImgURL = googleResponse.body.items[1].link; - const redirectOptions = { follow_max: 2 }; - const taskImgResponse = await needle('get', taskImgURL, null, redirectOptions); - const hintImgResponse = await needle('get', hintImgURL, null, redirectOptions); - taskImgBuffer = taskImgResponse.body; - hintImgBuffer = hintImgResponse.body; -} - -function randomInteger(min, max) { - let rand = min + Math.random() * (max + 1 - min); - rand = Math.floor(rand); - return rand; -} - -function getLettersHintMessage() { - const lettersAmountInfo = `${answer.length} ${getPluralForm(answer.length, 'буква', 'буквы', 'букв')}`; - return `В моём слове ${lettersAmountInfo}, первая — ${answer[0].toUpperCase()}`; -} - -async function giveHint(previousGameId) { - - // TODO: больше сообщений подсказок - let hintMessages = [ - 'Никто не знает? 😒 Вот подсказка!', - 'Я не думал, что будет так сложно... 😥 Держите подсказку', - ]; - - await vk.sendMessage(hintMessages[Math.floor(Math.random() * hintMessages.length)]); - await vk.sendPhotoToChat(hintImgBuffer); - if (!lettersHintSent) { - await vk.sendMessage(getLettersHintMessage()); - } - - if (previousGameId === gameId) { - timeoutObj = setTimeout(() => handleGameLoss(previousGameId), STEP_INTERVAL); - } else { - console.log('previous game is over, no need to handle game loss'); - } -} - -async function handleGameLoss(previousGameId) { - if (previousGameId !== gameId) { - return; - } - // TODO: больше сообщений ответов - let answerMessages = [ - `Не разгадали? Это же ${answer}!`, - `⏱ Время истекло! Правильный ответ — ${answer}`, - ]; - - resetGame(); - - vk.sendMessage(answerMessages[Math.floor(Math.random() * answerMessages.length)]); -} - -function resetGame() { - isPlaying = false; - answer = ''; - gameId = ''; - taskImgBuffer = null; - hintImgBuffer = null; - lettersHintSent = false; -} - -async function handleCorrectAnswer() { - clearTimeout(timeoutObj); - const previousAnswer = answer; - resetGame(); - const name = await vk.getUserName(message.from_id); - let successMessages = [ - `Браво, ${name}! Моё слово — ${previousAnswer} 👏`, - `${name}, ты умница! 😃 На картинке ${previousAnswer}`, - `Правильно, ${name}! 👍 Это действительно ${previousAnswer}`, - `И в этом раунде побеждает ${name}, разгадав слово "${previousAnswer}"! 😎`, - `Я увидел правильный ответ — ${previousAnswer}! ${name}, как тебе это удаётся? 🙀`, - ]; - let successMessage = successMessages[Math.floor(Math.random() * successMessages.length)]; - vk.sendMessage(successMessage); - // TODO: отправлять стикер -} - -function handlePlayingState() { - let text = message.text.toLowerCase(); - - // Если игра уже идёт, но кто-то написал новый запрос на игру, - // нужно закончить обработку этого сообщения, чтобы не было наложений сценариев бота - if(isGameRequestMessage(text)) { - console.log('game is already running'); - return true; - } - - let answerIsCorrect = checkAnswer(text); - - if (answerIsCorrect) { - handleCorrectAnswer(); - return true; - } else { - let word = extractWord(text); - let botMentioned = isBotMentioned(text); - if (word && !botMentioned) { - admin.addWord(word, false); - } - return word && !botMentioned; - } -} - -function isBotMentioned(text) { - return text.toLowerCase().startsWith('бот,') || text.includes(`club${process.env.VK_GROUP_ID}`); -} - -function isGameRequestMessage(text) { - const _text = text.toLowerCase(); - let botMentioned = isBotMentioned(_text); - let gameRequested = - _text.includes(' игр') || - _text.includes('поигра') || - _text.includes('сыгра'); - return botMentioned && gameRequested; -} - -function checkAnswer(entered) { - // TODO: более щадящая и интеллектуальная проверка корректности ответа - return entered === answer.toLowerCase(); -} - -module.exports = function(_message) { - message = _message; - return handleMessage(); -}; diff --git a/app/src/game/game.test.js b/app/src/game/game.test.js deleted file mode 100644 index 21305c4..0000000 --- a/app/src/game/game.test.js +++ /dev/null @@ -1,366 +0,0 @@ -require('dotenv').config(); - -const messageWithGameRequest = { text: 'Бот, давай играть' }; - -const messageWithCorrectAnswer = { - text: 'абракадабра', - from_id: 1, -}; - -const messageWithCorrectAnswerInUppercase = { - text: 'АБРАКАДАБРА', - from_id: 1, -}; - -const messageWithIncorrectAnswer = { text: 'Попытка' }; -const commonMessageWithBotMention = { text: 'Бот, привет' }; -const messageWithoutText = { text: '' }; - -const messageWithAddWordRequest = { text: 'Бот, запомни слово покемон' }; -const messageWithDeleteWordRequest = { text: 'Бот, забудь слово покемон' }; - -const searchQuotaExceededResponse = { - error: { - errors: [{ - domain: 'usageLimits' - }] - } -}; - -const googleSearchResponse = { - items: [ - { link: 'https://www.example.com/result-1.jpg' }, - { link: 'https://www.example.com/result-2.jpg' }, - ] -}; - -const taskFromDb = { name: 'абракадабра' }; - -const originalSetTimeout = setTimeout; - -beforeEach(() => { - jest.useFakeTimers(); -}); - -afterEach(() => { - jest.clearAllMocks(); - jest.resetModules(); -}); - -// TODO: fix tests -xdescribe('Game', () => { - -test('When bot receives a game start request and game is not started, new game starts ' + - '(bot generates a task and sends a greeting message and a picture with task)', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - const db = require('../db'); - game(messageWithGameRequest); - await waitMs(500); - expect(db.query).toHaveBeenCalledTimes(1); - expect(sender.sendMessage).toHaveBeenCalledTimes(1); - expect(sender.sendMessage.mock.calls[0][0]).toMatch(/Игра начинается/); - expect(sender.sendPhotoToChat).toHaveBeenCalledTimes(1); -}); - -test('When bot receives a game start request and game is not started, bot does not pass the message further', () => { - setMocks(); - const game = require('./game'); - expect(game(messageWithGameRequest)).toBe(true); -}); - -test('When bot receives a game start request and game is running, new game does not start ' + - '(bot does not generate a new task, does not send a greeting message and does not send a picture with task)', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - const db = require('../db'); - game(messageWithGameRequest); - game(messageWithGameRequest); - await waitMs(500); - expect(db.query).toHaveBeenCalledTimes(1); - expect(sender.sendMessage).toHaveBeenCalledTimes(1); - expect(sender.sendPhotoToChat).toHaveBeenCalledTimes(1); -}); - -test('When bot receives a game start request and game is running, bot does not pass the message further', () => { - setMocks(); - const game = require('./game'); - game(messageWithGameRequest); - expect(game(messageWithGameRequest)).toBe(true); -}); - -test('When a game is running and bot receives correct answer, bot sends a success message with winner name', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(100); - game(messageWithCorrectAnswer); - await waitMs(500); - expect(sender.getUserName).toHaveBeenCalledWith(1); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/Евлампий/); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/абракадабра/); -}); - -test('When a game is running and bot receives correct answer, ' + - 'the game stops (no new hints are sent, no new words messages are handled)', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - const admin = require('./admin'); - game(messageWithGameRequest); - await waitMs(100); - game(messageWithCorrectAnswer); - game(messageWithIncorrectAnswer); - jest.runAllTimers(); - await waitMs(500); - expect(sender.getUserName).toHaveBeenCalledTimes(1); - expect(sender.sendMessage).toHaveBeenCalledTimes(2); - expect(sender.sendPhotoToChat).toHaveBeenCalledTimes(1); - expect(admin.addWord).not.toHaveBeenCalled(); -}); - -test('Bot can recognize a correct answer in uppercase', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(100); - game(messageWithCorrectAnswerInUppercase); - await waitMs(500); - expect(sender.getUserName).toHaveBeenCalledWith(1); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/Евлампий/); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/абракадабра/); -}); - -test('When a game is running and bot receives more than one correct answer, ' + - 'bot sends only one success message with first winner name', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(100); - game(messageWithCorrectAnswer); - game(messageWithCorrectAnswer); - await waitMs(500); - expect(sender.getUserName).toHaveBeenCalledWith(1); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/Евлампий/); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/абракадабра/); - expect(sender.getUserName).toHaveBeenCalledTimes(1); - expect(sender.sendMessage).toHaveBeenCalledTimes(2); -}); - -test('When a game is running and bot receives a common message without correct answer, ' + - 'bot extracts a word and adds it to approval list', async () => { - setMocks(); - const game = require('./game'); - const admin = require('./admin'); - game(messageWithGameRequest); - await waitMs(100); - game(messageWithIncorrectAnswer); - expect(admin.addWord).toHaveBeenCalledTimes(1); - expect(admin.addWord).toHaveBeenLastCalledWith('попытка', false); -}); - -test('When a game is running and bot receives a message with bot mentioning and it is not a word addition or deletion request, ' + - 'bot passes this message further', async () => { - setMocks(); - const game = require('./game'); - game(messageWithGameRequest); - await waitMs(100); - expect(game(commonMessageWithBotMention)).toBe(false); -}); - -test('When a game is running and bot receives a message without text, bot passes this message further', async () => { - setMocks(); - const game = require('./game'); - game(messageWithGameRequest); - await waitMs(100); - expect(game(messageWithoutText)).toBe(false); -}); - -test('When a game is running and bot does not get correct answer during first round, bot sends a hint', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(100); - jest.runOnlyPendingTimers(); - await waitMs(500); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/подсказка/); - expect(sender.sendPhotoToChat).toHaveBeenCalledTimes(2); -}); - -test('When a game is running and bot does not get correct answer during second round, ' + - 'bot sends a failure info and a correct answer', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(100); - jest.runAllTimers(); - await waitMs(10); - jest.runAllTimers(); - await waitMs(500); - expect(sender.sendMessage).toHaveBeenCalledTimes(4); - expect(sender.sendMessage.mock.calls[3][0]).toMatch(/Не разгадали?/); - expect(sender.sendMessage.mock.calls[3][0]).toMatch(/абракадабра/); -}); - -test('When a game is running and bot does not get correct answer during second round, ' + - 'the game stops (no new hints are sent, no new words messages are handled)', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - const admin = require('./admin'); - game(messageWithGameRequest); - await waitMs(100); - jest.runAllTimers(); - await waitMs(10); - jest.runAllTimers(); - game(messageWithIncorrectAnswer); - game(messageWithCorrectAnswer); - await waitMs(500); - expect(sender.sendMessage).toHaveBeenCalledTimes(4); - expect(sender.sendMessage.mock.calls[3][0]).not.toMatch(/И в этом раунде/); - expect(sender.sendPhotoToChat).toHaveBeenCalledTimes(2); - expect(sender.getUserName).toHaveBeenCalledTimes(0); - expect(admin.addWord).toHaveBeenCalledTimes(0); -}); - -test('Bot can send an additional hint with first letter in first round', async () => { - setMocks({ letterHintInFirstRound: true }); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(100); - jest.runAllTimers(); - await waitMs(10); - jest.runAllTimers(); - await waitMs(500); - expect(sender.sendMessage).toHaveBeenCalledTimes(4); - expect(sender.sendMessage.mock.calls[1][0]).toMatch(/В моём слове 11 букв, первая — А/); -}); - -test('If bot did not send an additional hint with first letter in first round, ' + - 'it sends additional hint with first letter in second round', async () => { - setMocks({ letterHintInFirstRound: false }); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(100); - jest.runAllTimers(); - await waitMs(10); - jest.runAllTimers(); - await waitMs(500); - expect(sender.sendMessage).toHaveBeenCalledTimes(4); - expect(sender.sendMessage.mock.calls[2][0]).toMatch(/В моём слове 11 букв, первая — А/); -}); - -test('When bot receives a game start request and game is not started and daily quota of pictures search is exceeded, ' + - 'new game does not start (task is not sent)', async () => { - setMocks({ searchQuotaExceeded: true }); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(500); - expect(sender.sendPhotoToChat).toHaveBeenCalledTimes(0); -}); - -test('When bot receives a game start request and game is not started and daily quota of pictures search is exceeded, ' + - 'bot sends a \'tired\' sticker and a refusal message', async () => { - setMocks({ searchQuotaExceeded: true }); - const game = require('./game'); - const sender = require('../vk/vk'); - game(messageWithGameRequest); - await waitMs(500); - expect(sender.sendMessage).toHaveBeenCalledTimes(1); - expect(sender.sendMessage.mock.calls[0][0]).toMatch(/устал/); - expect(sender.sendSticker).toHaveBeenCalledTimes(1); - expect(sender.sendSticker.mock.calls[0][0]).toBe(13); -}); - -test('When bot receives a word addition request, bot extracts a word, remembers it and sends a success message', async () => { - setMocks(); - const game = require('./game'); - const sender = require('../vk/vk'); - const admin = require('./admin'); - game(messageWithAddWordRequest); - await waitMs(100); - expect(admin.addWord).toHaveBeenCalledTimes(1); - expect(admin.addWord).toHaveBeenCalledWith('покемон'); - expect(sender.sendMessage).toHaveBeenCalledTimes(1); - expect(sender.sendMessage.mock.calls[0][0]).toMatch(/Я запомнил слово "покемон"/); -}); - -test('When bot receives a word addition request and this word exists already, ' + - 'bot sends a message with information about duplicated word', async () => { - setMocks({ duplicatedWord: true }); - const game = require('./game'); - const sender = require('../vk/vk'); - const admin = require('./admin'); - game(messageWithAddWordRequest); - await waitMs(100); - expect(admin.addWord).toHaveBeenCalledTimes(1); - expect(admin.addWord).toHaveBeenCalledWith('покемон'); - expect(sender.sendMessage).toHaveBeenCalledTimes(1); - expect(sender.sendMessage.mock.calls[0][0]).toMatch(/Я уже знаю слово "покемон"/); -}); - -test('When bot receives a word deletion request, bot extracts a word, deletes it and sends a success message', async () => { - setMocks({ duplicatedWord: true }); - const game = require('./game'); - const sender = require('../vk/vk'); - const admin = require('./admin'); - game(messageWithDeleteWordRequest); - await waitMs(100); - expect(admin.deleteWord).toHaveBeenCalledTimes(1); - expect(admin.deleteWord).toHaveBeenCalledWith('покемон'); - expect(sender.sendMessage).toHaveBeenCalledTimes(1); - expect(sender.sendMessage.mock.calls[0][0]).toMatch(/Я забыл слово "покемон"/); -}); - - -function setMocks(options) { - jest.doMock('needle', () => (method, url) => { - if (url.includes('google')) { - return Promise.resolve({ - body: options?.searchQuotaExceeded ? searchQuotaExceededResponse : googleSearchResponse - }); - } - if (url.includes('jpg')) { - return Promise.resolve({}); - } - }); - - jest.doMock('../../vk'); - const sender = require('../vk/vk'); - sender.sendMessage.mockResolvedValue('ok'); - sender.sendPhotoToChat.mockResolvedValue('ok'); - sender.getUserName - .mockResolvedValueOnce('Евлампий') - .mockResolvedValueOnce('Афанасий'); - - jest.doMock('../db'); - const db = require('../db'); - db.query.mockImplementation(_query => { - if (_query.includes('SELECT')) { - return Promise.resolve({ rows: [ taskFromDb ]}); - } - }); - - jest.doMock('./admin'); - const admin = require('./admin'); - admin.addWord.mockResolvedValue(options?.duplicatedWord ? '23505' : null); - admin.deleteWord.mockResolvedValue(null); - - jest.spyOn(global.Math, 'random').mockReturnValue(options?.letterHintInFirstRound ? 0.9 : 0.1); -} - -async function waitMs(ms) { - return new Promise(resolve => originalSetTimeout(() => resolve(), ms)); -} - -}); diff --git a/app/src/game/game.test.ts b/app/src/game/game.test.ts new file mode 100644 index 0000000..90dcc52 --- /dev/null +++ b/app/src/game/game.test.ts @@ -0,0 +1,270 @@ +import {config} from 'dotenv'; +import game from './game'; +import db from '../db'; +import vk from '../vk/vk'; +import admin from './admin'; +import * as testMessages from './test-resources/messages'; +import {googleSearchSuccessResponse, googleSearchQuotaExceededResponse} from './test-resources/google-responses'; +import {QueryResult} from 'pg'; +import {AddWordResult} from './model/AddWordResult'; +import SpyInstance = jest.SpyInstance; + +config(); + +const taskFromDb = { name: 'абракадабра' }; +const googleSearchResponse: { body?: unknown } = { }; + +jest.useFakeTimers('modern'); +jest.mock('../vk/vk'); +jest.mock('../db'); +jest.mock('./admin'); +jest.mock('needle', () => (method: string, url: string) => { + if (url.includes('google')) { + return Promise.resolve(googleSearchResponse); + } + if (url.includes('jpg')) { + return Promise.resolve({}); + } +}); + +const sendMessageSpy = jest.spyOn(vk, 'sendMessage').mockResolvedValue(true); +const sendPhotoToChatSpy = jest.spyOn(vk, 'sendPhotoToChat').mockResolvedValue(); +const getUserNameSpy = jest.spyOn(vk, 'getUserName').mockResolvedValue('Евлампий'); +const sendStickerSpy = jest.spyOn(vk, 'sendSticker').mockResolvedValue(true); + +const dbQuerySpy = jest.spyOn(db, 'query').mockImplementation(_query => { + return Promise.resolve({ rows: _query.includes('SELECT') ? [ taskFromDb ] : []} as QueryResult); +}); + +let adminAddWordSpy: SpyInstance; +const adminDeleteWordSpy = jest.spyOn(admin,'deleteWord').mockResolvedValue(); + +describe('Game', () => { + afterEach(async () => { + await endCurrentRound(); + await endCurrentRound(); + }); + + describe('Game start request handling', () => { + beforeEach(() => setMocks({ letterHintInFirstRound: false })); + + test('When bot receives a game start request and game is not started, new game starts ' + + '(bot generates a task and sends a greeting message and a picture with task)', async () => { + await game(testMessages.messageWithGameRequest); + expect(dbQuerySpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy.mock.calls[0][0]).toMatch(/Игра начинается/); + expect(sendPhotoToChatSpy).toHaveBeenCalledTimes(1); + }); + + test('When bot receives a game start request and game is not started, bot does not pass the message further', async () => { + expect(await game(testMessages.messageWithGameRequest)).toBe(true); + }); + + test('When bot receives a game start request and game is running, new game does not start ' + + '(bot does not generate a new task, does not send a greeting message and does not send a picture with task)', async () => { + await game(testMessages.messageWithGameRequest); + await game(testMessages.messageWithGameRequest); + expect(dbQuerySpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendPhotoToChatSpy).toHaveBeenCalledTimes(1); + }); + + test('When bot receives a game start request and game is running, bot does not pass the message further', async () => { + await game(testMessages.messageWithGameRequest); + expect(await game(testMessages.messageWithGameRequest)).toBe(true); + }); + }); + + describe('Win messages handling', () => { + test('When a game is running and bot receives correct answer, bot sends a success message with winner name', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await game(testMessages.messageWithCorrectAnswer); + expect(getUserNameSpy).toHaveBeenCalledWith(1); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/Евлампий/); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/абракадабра/); + }); + + test('When a game is running and bot receives correct answer, ' + + 'the game stops (no new hints are sent, no new words messages are handled)', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await game(testMessages.messageWithCorrectAnswer); + jest.clearAllMocks(); + await endCurrentRound(); + expect(getUserNameSpy).not.toHaveBeenCalled(); + expect(sendMessageSpy).not.toHaveBeenCalled(); + expect(sendPhotoToChatSpy).not.toHaveBeenCalled(); + expect(adminAddWordSpy).not.toHaveBeenCalled(); + }); + + test('Bot can recognize a correct answer in uppercase', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await game(testMessages.messageWithCorrectAnswerInUppercase); + expect(getUserNameSpy).toHaveBeenCalledWith(1); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/Евлампий/); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/абракадабра/); + }); + + test('When a game is running and bot receives more than one correct answer, ' + + 'bot sends only one success message with first winner name', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await game(testMessages.messageWithCorrectAnswer); + await game(testMessages.messageWithCorrectAnswer); + expect(getUserNameSpy).toHaveBeenCalledWith(1); + expect(sendMessageSpy).toHaveBeenCalledTimes(2); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/Евлампий/); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/абракадабра/); + }); + }); + + describe('Failure handling', function () { + it('When a game is running and bot does not get correct answer during second round, ' + + 'bot sends a failure info and a correct answer', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await endCurrentRound(); + await endCurrentRound(); + expect(sendMessageSpy).toHaveBeenCalledTimes(4); + expect(sendMessageSpy.mock.calls[3][0]).toMatch(/Не разгадали?/); + expect(sendMessageSpy.mock.calls[3][0]).toMatch(/абракадабра/); + }); + + test('When a game is running and bot does not get correct answer during second round, ' + + 'the game stops (no new hints are sent, no new words messages are handled)', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await endCurrentRound(); + await endCurrentRound(); + jest.clearAllMocks(); + await game(testMessages.messageWithIncorrectAnswer); + await game(testMessages.messageWithCorrectAnswer); + expect(sendMessageSpy).not.toHaveBeenCalled(); + expect(sendPhotoToChatSpy).not.toHaveBeenCalled(); + expect(getUserNameSpy).not.toHaveBeenCalled(); + expect(adminAddWordSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Attempt messages handling', () => { + test('When a game is running and bot receives a common message without correct answer, ' + + 'bot extracts a word and adds it to approval list', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await game(testMessages.messageWithIncorrectAnswer); + expect(adminAddWordSpy).toHaveBeenCalledTimes(1); + expect(adminAddWordSpy).toHaveBeenCalledWith('попытка', false); + }); + }); + + describe('Handling of messages not related to game', () => { + test('When a game is running and bot receives a message with bot mentioning and it is not a word addition or deletion request, ' + + 'bot passes this message further', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + expect(await game(testMessages.commonMessageWithBotMention)).toBe(false); + }); + + test('When a game is running and bot receives a message without text, bot passes this message further', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + expect(await game(testMessages.messageWithoutText)).toBe(false); + }); + }); + + describe('Hints handling', () => { + test('When a game is running and bot does not get correct answer during first round, bot sends a hint', async () => { + setMocks(); + await game(testMessages.messageWithGameRequest); + await endCurrentRound(); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/подсказка/); + expect(sendPhotoToChatSpy).toHaveBeenCalledTimes(2); + }); + + test('Bot can send an additional hint with first letter in first round', async () => { + setMocks({ letterHintInFirstRound: true }); + await game(testMessages.messageWithGameRequest); + expect(sendMessageSpy.mock.calls[1][0]).toMatch(/В моём слове 11 букв, первая — А/); + }); + + test('If bot did not send an additional hint with first letter in first round, ' + + 'it sends additional hint with first letter in second round', async () => { + setMocks({ letterHintInFirstRound: false }); + await game(testMessages.messageWithGameRequest); + await endCurrentRound(); + expect(sendMessageSpy.mock.calls[2][0]).toMatch(/В моём слове 11 букв, первая — А/); + }); + }); + + describe('Google Search daily quota excess handling', () => { + test('When bot receives a game start request and game is not started and daily quota of pictures search is exceeded, ' + + 'new game does not start (task is not sent)', async () => { + setMocks({ searchQuotaExceeded: true }); + await game(testMessages.messageWithGameRequest); + expect(sendPhotoToChatSpy).not.toHaveBeenCalled(); + }); + + test('When bot receives a game start request and game is not started and daily quota of pictures search is exceeded, ' + + 'bot sends a \'tired\' sticker and a refusal message', async () => { + setMocks({ searchQuotaExceeded: true }); + await game(testMessages.messageWithGameRequest); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy.mock.calls[0][0]).toMatch(/устал/); + expect(sendStickerSpy).toHaveBeenCalledTimes(1); + expect(sendStickerSpy.mock.calls[0][0]).toBe(13); + }); + }); + + describe('Word addition/deletion requests handling', () => { + test('When bot receives a word addition request, bot extracts a word, remembers it and sends a success message', async () => { + setMocks(); + await game(testMessages.messageWithAddWordRequest); + expect(adminAddWordSpy).toHaveBeenCalledTimes(1); + expect(adminAddWordSpy).toHaveBeenCalledWith('покемон', true); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy.mock.calls[0][0]).toMatch(/Я запомнил слово "покемон"/); + }); + + test('When bot receives a word addition request and this word exists already, ' + + 'bot sends a message with information about duplicated word', async () => { + setMocks({ duplicatedWord: true }); + await game(testMessages.messageWithAddWordRequest); + expect(adminAddWordSpy).toHaveBeenCalledTimes(1); + expect(adminAddWordSpy).toHaveBeenCalledWith('покемон', true); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy.mock.calls[0][0]).toMatch(/Я уже знаю слово "покемон"/); + }); + + test('When bot receives a word deletion request, bot extracts a word, deletes it and sends a success message', async () => { + setMocks(); + await game(testMessages.messageWithDeleteWordRequest); + expect(adminDeleteWordSpy).toHaveBeenCalledTimes(1); + expect(adminDeleteWordSpy).toHaveBeenCalledWith('покемон'); + expect(sendMessageSpy).toHaveBeenCalledTimes(1); + expect(sendMessageSpy.mock.calls[0][0]).toMatch(/Я забыл слово "покемон"/); + }); + }); + +}); + +function setMocks(options?: { + searchQuotaExceeded?: boolean, + duplicatedWord?: boolean, + letterHintInFirstRound?: boolean, +}) { + googleSearchResponse.body = options?.searchQuotaExceeded ? googleSearchQuotaExceededResponse : googleSearchSuccessResponse; + adminAddWordSpy = jest.spyOn(admin,'addWord') + .mockResolvedValue(options?.duplicatedWord ? AddWordResult.DUPLICATE_WORD : AddWordResult.SUCCESS); + + jest.spyOn(global.Math, 'random').mockReturnValue(options?.letterHintInFirstRound ? 0.9 : 0.1); +} + +async function endCurrentRound() { + jest.runOnlyPendingTimers(); + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } +} diff --git a/app/src/game/game.ts b/app/src/game/game.ts new file mode 100644 index 0000000..9128490 --- /dev/null +++ b/app/src/game/game.ts @@ -0,0 +1,276 @@ +import needle from 'needle'; +import {config} from 'dotenv'; +import vk from '../vk/vk'; +import dbClient from '../db'; +import admin from './admin'; +import {getPluralForm} from '../util'; +import {VkMessage} from '../vk/model/VkMessage'; +import {AddWordResult} from './model/AddWordResult'; + +config(); + +const STEP_INTERVAL = process.env.GAME_STEP_INTERVAL ? parseInt(process.env.GAME_STEP_INTERVAL) : 15000; + +let message: VkMessage; +let answer = ''; +let isPlaying = false; +let gameId = ''; +let timeoutObj: NodeJS.Timeout; +let taskImgBuffer: Buffer | null; +let hintImgBuffer: Buffer | null; +let lettersHintSent = false; + +async function getRandomTask(): Promise { + const query = ` + SELECT name FROM friends_vk_bot.words WHERE approved = true + ORDER BY RANDOM() LIMIT 1; + `; + + const dbResult = await dbClient.query(query); + return dbResult.rows[0].name; +} + +async function handleMessage(_message: VkMessage): Promise { + message = _message; + if (!message.text) { + return false; + } + if (isAddWordRequest(message.text)) { + await handleAddWordRequest(message.text); + return true; + } + if (isDeleteWordRequest(message.text)) { + await handleDeleteWordRequest(message.text); + return true; + } + if (isPlaying) { + return await handlePlayingState(); + } + if (isGameRequestMessage(message.text)) { + await handleGameRequestMessage(); + return true; + } + return false; +} + +function isAddWordRequest(text: string): boolean { + const botMentioned = isBotMentioned(text); + const containsAddWordRequest = text.includes('запомни слово'); + return botMentioned && containsAddWordRequest; +} + +async function handleAddWordRequest(text: string): Promise { + const word = extractWord(text); + + if (word) { + const result = await admin.addWord(word, true); + if (result === AddWordResult.DUPLICATE_WORD) { + await vk.sendMessage(`Я уже знаю слово "${ word }"! 😊`, 3000); + } else if (result === AddWordResult.SUCCESS) { + await vk.sendMessage(`👍 Я запомнил слово "${ word }"!`, 3000); + } + } else { + const userName = await vk.getUserName(message.from_id); + await vk.sendMessage(`${userName}, я тебя не понимаю 😒`, 3000); + } +} + +function isDeleteWordRequest(text: string): boolean { + const botMentioned = isBotMentioned(text); + const containsDeleteWordRequest = text.includes('забудь слово'); + return botMentioned && containsDeleteWordRequest; +} + +async function handleDeleteWordRequest(text: string): Promise { + const word = extractWord(text); + + if (word) { + await admin.deleteWord(word); + await vk.sendMessage(`👍 Я забыл слово "${ word }"!`, 3000); + } else { + const userName = await vk.getUserName(message.from_id); + await vk.sendMessage(`${userName}, я тебя не понимаю 😒`, 3000); + } +} + +function extractWord(text: string): string | null { + const parts = text.split(' '); + const word = parts[parts.length - 1].replace(/[.,/#!$%^&*;:{}=\-_`~()a-zA-Z\d]/g, ''); + return word === '' ? null : word; +} + +async function handleGameRequestMessage(): Promise { + + if (isPlaying) { + console.log('Game is already running, cannot start new game'); + return; + } + + isPlaying = true; + gameId = Date.now().toString(); + answer = await getRandomTask(); + console.log(`Correct answer: ${answer}`); + try { + await generatePhotos(answer); + const welcomeMessages = [ + 'Игра начинается, отгадывать могут все! 😏 Какое слово я загадал?', + 'Я люблю играть! 😊 Я загадал слово, которое описывает эту картинку. Сможете угадать это слово?', + ]; + await vk.sendMessage(welcomeMessages[Math.floor(Math.random() * welcomeMessages.length)], 3000); + await vk.sendPhotoToChat(taskImgBuffer!); + if (Math.random() > 0.5) { + await vk.sendMessage(getLettersHintMessage()); + lettersHintSent = true; + } + timeoutObj = setTimeout(async () => await giveHint(gameId), STEP_INTERVAL); + } catch (error) { + resetGame(); + + if (error instanceof Error && error.message === 'usageLimits') { + const limitsMessages = [ + 'Что-то я устал играть... 😫 Приходите завтра 😊', + 'Давай продолжим завтра? Сегодня больше не хочется 😳', + 'Я уже наигрался, мне надо отдохнуть', + ]; + + const limitsStickers = [13, 85, 2091, 5135, 5629]; + + await vk.sendSticker(limitsStickers[Math.floor(Math.random() * limitsStickers.length)]); + await vk.sendMessage(limitsMessages[Math.floor(Math.random() * limitsMessages.length)], 5000); + } else { + console.log(error); + } + } +} + +async function generatePhotos(answer: string): Promise { + const apiURL = 'https://www.googleapis.com/customsearch/v1'; + const key = process.env.GOOGLE_KEY; + const cx = process.env.GOOGLE_SEARCH_ENGINE_ID; + const start = randomInteger(1, 5); + + const url = `${apiURL}?q=${encodeURIComponent(answer)}&cx=${cx}&fileType=jpg&num=2&safe=active&searchType=image&fields=items%2Flink&start=${start}&key=${key}`; + const googleResponse = await needle('get', url); + console.log(googleResponse.body); + if (googleResponse.body.error?.errors[0].domain === 'usageLimits') { + throw new Error('usageLimits'); + } + const taskImgURL = googleResponse.body.items[0].link; + const hintImgURL = googleResponse.body.items[1].link; + const redirectOptions = { follow_max: 2 }; + const taskImgResponse = await needle('get', taskImgURL, null, redirectOptions); + const hintImgResponse = await needle('get', hintImgURL, null, redirectOptions); + console.log('Task and hint images are downloaded'); + taskImgBuffer = taskImgResponse.body; + hintImgBuffer = hintImgResponse.body; +} + +function randomInteger(min: number, max: number): number { + return Math.floor(min + Math.random() * (max + 1 - min)); +} + +function getLettersHintMessage(): string { + const lettersAmountInfo = `${answer.length} ${getPluralForm(answer.length, 'буква', 'буквы', 'букв')}`; + return `В моём слове ${lettersAmountInfo}, первая — ${answer[0].toUpperCase()}`; +} + +async function giveHint(previousGameId: string): Promise { + console.log('Giving hint...'); + const hintMessages = [ + 'Никто не знает? 😒 Вот подсказка!', + 'Я не думал, что будет так сложно... 😥 Держите подсказку', + ]; + + await vk.sendMessage(hintMessages[Math.floor(Math.random() * hintMessages.length)]); + await vk.sendPhotoToChat(hintImgBuffer!); + if (!lettersHintSent) { + await vk.sendMessage(getLettersHintMessage()); + } + + if (previousGameId === gameId) { + timeoutObj = setTimeout(async () => { + await handleGameLoss(previousGameId); + }, STEP_INTERVAL); + } else { + console.log('Previous game is over, no need to handle game loss'); + } +} + +async function handleGameLoss(previousGameId: string): Promise { + console.log('Handling game loss...'); + if (previousGameId !== gameId) { + return; + } + const answerMessages = [ + `Не разгадали? Это же ${answer}!`, + `⏱ Время истекло! Правильный ответ — ${answer}`, + ]; + + resetGame(); + + await vk.sendMessage(answerMessages[Math.floor(Math.random() * answerMessages.length)]); +} + +function resetGame():void { + isPlaying = false; + answer = ''; + gameId = ''; + taskImgBuffer = null; + hintImgBuffer = null; + lettersHintSent = false; +} + +async function handleCorrectAnswer(): Promise { + clearTimeout(timeoutObj); + const previousAnswer = answer; + resetGame(); + const name = await vk.getUserName(message.from_id); + const successMessages = [ + `Браво, ${name}! Моё слово — ${previousAnswer} 👏`, + `${name}, ты умница! 😃 На картинке ${previousAnswer}`, + `Правильно, ${name}! 👍 Это действительно ${previousAnswer}`, + `И в этом раунде побеждает ${name}, разгадав слово "${previousAnswer}"! 😎`, + `Я увидел правильный ответ — ${previousAnswer}! ${name}, как тебе это удаётся? 🙀`, + ]; + const successMessage = successMessages[Math.floor(Math.random() * successMessages.length)]; + await vk.sendMessage(successMessage); +} + +async function handlePlayingState(): Promise { + const text = message.text.toLowerCase(); + + // Если игра уже идёт, но кто-то написал новый запрос на игру, + // нужно закончить обработку этого сообщения, чтобы не было наложений сценариев бота + if (isGameRequestMessage(text)) { + console.log('Game is already running, stop passing further'); + return true; + } + + if (text === answer.toLowerCase()) { + await handleCorrectAnswer(); + return true; + } else { + const word = extractWord(text); + const botMentioned = isBotMentioned(text); + if (word && !botMentioned) { + await admin.addWord(word, false); + } + return !!word && !botMentioned; + } +} + +function isBotMentioned(text: string): boolean { + return text.toLowerCase().startsWith('бот,') || text.includes(`club${process.env.VK_GROUP_ID}`); +} + +function isGameRequestMessage(text: string): boolean { + const _text = text.toLowerCase(); + const botMentioned = isBotMentioned(_text); + const gameRequested = + _text.includes(' игр') || + _text.includes('поигра') || + _text.includes('сыгра'); + return botMentioned && gameRequested; +} + +export default handleMessage; diff --git a/app/src/game/model/AddWordResult.ts b/app/src/game/model/AddWordResult.ts new file mode 100644 index 0000000..b2030fa --- /dev/null +++ b/app/src/game/model/AddWordResult.ts @@ -0,0 +1,5 @@ +export enum AddWordResult { + SUCCESS, + DUPLICATE_WORD, + ERROR, +} diff --git a/app/src/game/test-resources/google-responses.ts b/app/src/game/test-resources/google-responses.ts new file mode 100644 index 0000000..eb395da --- /dev/null +++ b/app/src/game/test-resources/google-responses.ts @@ -0,0 +1,14 @@ +export const googleSearchQuotaExceededResponse = { + error: { + errors: [{ + domain: 'usageLimits' + }] + } +}; + +export const googleSearchSuccessResponse = { + items: [ + { link: 'https://www.example.com/result-1.jpg' }, + { link: 'https://www.example.com/result-2.jpg' }, + ] +}; diff --git a/app/src/game/test-resources/messages.ts b/app/src/game/test-resources/messages.ts new file mode 100644 index 0000000..b03a268 --- /dev/null +++ b/app/src/game/test-resources/messages.ts @@ -0,0 +1,35 @@ +import {VkMessage} from '../../vk/model/VkMessage'; + +export const messageWithGameRequest: VkMessage = { + text: 'Бот, давай играть' +} as VkMessage; + +export const messageWithCorrectAnswer: VkMessage = { + text: 'абракадабра', + from_id: 1, +} as VkMessage; + +export const messageWithCorrectAnswerInUppercase: VkMessage = { + text: 'АБРАКАДАБРА', + from_id: 1, +} as VkMessage; + +export const messageWithIncorrectAnswer: VkMessage = { + text: 'Попытка' +} as VkMessage; + +export const commonMessageWithBotMention: VkMessage = { + text: 'Бот, привет' +} as VkMessage; + +export const messageWithoutText: VkMessage = { + text: '' +} as VkMessage; + +export const messageWithAddWordRequest: VkMessage = { + text: 'Бот, запомни слово покемон' +} as VkMessage; + +export const messageWithDeleteWordRequest: VkMessage = { + text: 'Бот, забудь слово покемон' +} as VkMessage; diff --git a/package-lock.json b/package-lock.json index 43756b0..9fada71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,7 @@ "npm-watch": "^0.11.0", "pg": "^8.7.1", "react": "^17.0.2", - "react-dom": "^17.0.2", - "uuid": "^8.3.2" + "react-dom": "^17.0.2" }, "devDependencies": { "@babel/cli": "^7.15.4", @@ -15716,14 +15715,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -28231,11 +28222,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", diff --git a/package.json b/package.json index 1570e1d..6dac260 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ "npm-watch": "^0.11.0", "pg": "^8.7.1", "react": "^17.0.2", - "react-dom": "^17.0.2", - "uuid": "^8.3.2" + "react-dom": "^17.0.2" }, "devDependencies": { "@babel/cli": "^7.15.4",