diff --git a/package.json b/package.json
index 9cd6b26..def5640 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sockmafia",
- "version": "4.0.3",
+ "version": "4.1.3",
"description": "Mafia plugin for sockbot",
"main": "src/mafiabot.js",
"scripts": {
@@ -27,6 +27,7 @@
},
"homepage": "https://github.com/yamikuronue/sockMafia#readme",
"dependencies": {
+ "axios": "^0.16.2",
"bluebird": "^3.1.1",
"debug": "^2.2.0",
"fs-readfile-promise": "^3.0.0",
@@ -46,6 +47,7 @@
"istanbul": "^0.4.1",
"jsdoc-to-markdown": "^2.0.1",
"mocha": "^2.3.4",
+ "nock": "^9.0.14",
"publish-please": "2.1.4",
"sinon": "^1.15.1",
"sinon-as-promised": "^4.0.0",
diff --git a/src/mod_controller.js b/src/mod_controller.js
index 2cc4b90..3fa3976 100644
--- a/src/mod_controller.js
+++ b/src/mod_controller.js
@@ -11,6 +11,7 @@ const view = require('./view');
const Promise = require('bluebird');
const debug = require('debug')('sockbot:mafia:modController');
const Utils = require('./utils');
+const axios = require('axios');
exports.internals = {};
@@ -179,6 +180,10 @@ class MafiaModController {
'You are a **cop**! Each night you can investigate one person using `!target playerName in TargetGame`.\n' +
'!send-rolecard TargetUsername in TargetGame\n' +
'```\n');
+
+ forum.Commands.add('endGame', 'End the game and declare the winner (mod only)', this.endHandler.bind(this));
+ forum.Commands.addExtendedHelp('endGame', 'End the game\n\n' +
+ 'Usage: `!endGame [winner]`');
}
@@ -722,6 +727,47 @@ class MafiaModController {
view.reportError(command, 'Error listing night actions: ', err);
});
}
+
+ endHandler(command) {
+ let game, mod;
+ let winner = command.args[0];
+
+ if (!winner) {
+ view.reportError(command, 'No winner set. Please use `!winner scum`, `!winner town`, or `!winner 3party` to declare a winner');
+ return Promise.resolve();
+ }
+
+ winner = winner.toLowerCase();
+
+ if (['town', 'scum', '3party'].indexOf(winner) === -1) {
+ view.reportError(command, 'No winner set. Please use `!winner scum`, `!winner town`, or `!winner 3party` to declare a winner');
+ return Promise.resolve();
+ }
+
+
+ return Promise.all([this.getGame(command), command.getUser()])
+ .then((responses) => {
+ game = responses[0];
+
+ debug('Ending game ' + game.id);
+ try {
+ mod = game.getModerator(responses[1].username);
+ } catch (_) {
+ return Promise.reject(new Error('You are not a moderator'));
+ }
+ return mod.isModerator ? Promise.resolve() : Promise.reject('You are not a moderator');
+ })
+ .then(() => game.setActive(false))
+ .then(() => game.setValue('winner', winner))
+ .then(() => view.respondInThread(game.topicId, `The game is over! ${winner} has won!`))
+ .then(() => debug('about to send data'))
+ .then(() => axios.post('http://mafia.sockdrawer.io:5984/games', game.toJSON()))
+ .then(() => debug('data sent!'))
+ .catch((err) => {
+ logRecoveredError('Error ending game: ' + err);
+ view.reportError(command, 'Error ending game: ', err);
+ });
+ }
}
module.exports = MafiaModController;
diff --git a/test/integration/statsTests.js b/test/integration/statsTests.js
new file mode 100644
index 0000000..47578fe
--- /dev/null
+++ b/test/integration/statsTests.js
@@ -0,0 +1,199 @@
+'use strict';
+/*globals describe, it, beforeEach, afterEach, before*/
+
+const chai = require('chai'),
+ sinon = require('sinon'),
+ nock = require('nock');
+
+//do NOT hit real servers
+nock.disableNetConnect();
+
+//promise library plugins
+require('sinon-as-promised');
+const chaiAsPromised = require('chai-as-promised');
+chai.use(chaiAsPromised);
+
+chai.should();
+
+const PlayerController = require('../../src/player_controller');
+const ModController = require('../../src/mod_controller');
+const DAO = require('../../src/dao');
+const view = require('../../src/view.js');
+
+const testConfig = {
+ db: ':memory:',
+};
+
+const mockForum = {
+ Chat: {
+ create: () => 1
+ },
+ User: {
+ getByName: (user) => {
+ return user;
+ }
+ },
+ Post: {
+ reply: () => {
+ return Promise.resolve();
+ }
+ },
+ supports: (input) => {
+ return input === 'Chats' || input === 'Formatting.Markup.HTML' || input === 'Formatting.Multiline';
+ },
+ Format: {
+ urlForTopic: (topicId, slug, postId) => {
+ return '/t/' + slug + '/' + topicId + '/' + postId;
+ },
+ urlForPost: (postId) => {
+ return '/p/' + postId;
+ },
+ header2: (text) => `
${text}
`,
+ header3: (text) => `${text}
`,
+ bold: (text) => `${text}`,
+ link: (url, text) => `${text}`,
+ }
+};
+
+describe('MafiaStats', function () {
+ let mockCalls = [];
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = sinon.sandbox.create();
+ mockCalls = [];
+ });
+
+ afterEach(() => {
+ nock.cleanAll();
+ sandbox.restore();
+ });
+
+ describe('Basic game stats', () => {
+ let dao, playerController, modController, game;
+
+ before(() => {
+ //Set up the database
+ dao = new DAO(':memory:');
+ playerController = new PlayerController(dao, testConfig);
+ playerController.formatter = {
+ urlForPost: () => '',
+ quoteText: (input) => input
+ };
+ modController = new ModController(dao, testConfig);
+
+
+ view.activate(mockForum);
+
+ return dao.createGame(1, 'Game 1')
+ .then((g) => {
+ game = g;
+ sinon.stub(dao, 'getGameByTopicId').resolves(game);
+ return game.addPlayer('yamikuronue');
+ })
+ .then(() => game.addPlayer('accalia'))
+ .then(() => game.addPlayer('dreikin'))
+ .then(() => game.addPlayer('tehninja'))
+ .then(() => game.addModerator('moddyMcModFace'))
+ .then(() => game.newDay());
+ });
+
+ it('should record basic stats', () => {
+ let command = {
+ args: ['@accalia'],
+ input: '!vote @accalia',
+ reply: sandbox.stub(),
+ getTopic: () => Promise.resolve({id: 2}),
+ getPost: () => Promise.resolve({id: 1}),
+ getUser: () => Promise.resolve({username: 'yamikuronue'}),
+ parent: {
+ ids: {
+ topic: 1
+ }
+ }
+ };
+
+ mockCalls.push(nock('http://mafia.sockdrawer.io:5984')
+ .post('/games')
+ .reply(201, JSON.stringify({ok: true})));
+
+ //First, register a vote
+
+ return playerController.voteHandler(command).then(() => {
+ command = {
+ args: [],
+ input: '!unvote',
+ reply: sandbox.stub(),
+ getTopic: () => Promise.resolve({id: 2}),
+ getPost: () => Promise.resolve({id: 2}),
+ getUser: () => Promise.resolve({username: 'yamikuronue'}),
+ parent: {
+ ids: {
+ topic: 2
+ }
+ }
+ };
+
+ //Then, unvote
+ return playerController.unvoteHandler(command);
+ }).then(() => {
+ command.reply.lastCall.args[0].should.include('@yamikuronue unvoted');
+
+ command = {
+ args: ['@accalia'],
+ input: '!vote @accalia',
+ reply: sandbox.stub(),
+ getTopic: () => Promise.resolve({id: 2}),
+ getPost: () => Promise.resolve({id: 3}),
+ getUser: () => Promise.resolve({username: 'yamikuronue'}),
+ parent: {
+ ids: {
+ topic: 2
+ }
+ }
+ };
+
+ //Vote for the same person again
+ return playerController.voteHandler(command);
+ }).then(() => {
+
+ command = {
+ args: [],
+ input: '!unvote',
+ reply: sandbox.stub(),
+ getTopic: () => Promise.resolve({id: 2}),
+ getPost: () => Promise.resolve({id: 4}),
+ getUser: () => Promise.resolve({username: 'yamikuronue'}),
+ parent: {
+ ids: {
+ topic: 2
+ }
+ }
+ };
+
+ //Then unvote
+ return playerController.unvoteHandler(command);
+ }).then(() => {
+
+ command = {
+ args: ['scum'],
+ input: '!endGame',
+ reply: sandbox.stub(),
+ getTopic: () => Promise.resolve({id: 2}),
+ getPost: () => Promise.resolve({id: 5}),
+ getUser: () => Promise.resolve({username: 'moddyMcModFace'}),
+ parent: {
+ ids: {
+ topic: 2
+ }
+ }
+ };
+
+ return modController.endHandler(command);
+ }).then(() => {
+ mockCalls[0].isDone().should.be.true;
+ game.getValue('winner').should.equal('scum');
+ });
+ });
+ });
+});
diff --git a/test/unit/modControllerTest.js b/test/unit/modControllerTest.js
index 978a660..0d43fe8 100644
--- a/test/unit/modControllerTest.js
+++ b/test/unit/modControllerTest.js
@@ -1260,6 +1260,7 @@ describe('mod controller', () => {
});
});
});
+
describe('set()', () => {
let mockGame, mockUser, mockTarget, mockdao, modController;
@@ -2258,4 +2259,219 @@ describe('mod controller', () => {
});
});
});
+
+
+ describe('endGame()', () => {
+ let mockGame, mockUser, mockdao, modController, mockRequest;
+
+ beforeEach(() => {
+
+ mockUser = {
+ username: 'God',
+ getPlayerProperty: () => [],
+ isModerator: true
+ };
+
+
+ mockGame = {
+ isActive: true,
+ name: 'testMafia',
+ getAllPlayers: () => ['Rachel', 'Ross', 'Joey', 'Chandler', 'Phoebe', 'Monica'],
+ livePlayers: [mockUser, mockUser, mockUser],
+ killPlayer: () => Promise.resolve(),
+ nextPhase: () => Promise.resolve(),
+ newDay: () => Promise.resolve(),
+ setActive: () => Promise.resolve(),
+ toJSON: () => 1,
+ getActions: () => 1,
+ getPlayer: () => mockUser,
+ getModerator: () => mockUser,
+ topicId: 12,
+ day: 1,
+ setValue: () => Promise.resolve(),
+ phase: 'night'
+ };
+
+ mockdao = {
+ getGameByTopicId: () => Promise.resolve(mockGame),
+ getGameByChatId: () => Promise.resolve(mockGame)
+ };
+
+ modController = new ModController(mockdao);
+
+ mockRequest = sandbox.stub(require('axios'), 'post');
+ });
+
+ it('Should reject non-mods', () => {
+ const command = {
+ getTopic: () => Promise.resolve({
+ id: 12345
+ }),
+ getUser: () => Promise.resolve({
+ username: 'tehNinja'
+ }),
+ args: ['scum'],
+ parent: {
+ ids: {
+ topic: 12345
+ }
+ },
+ };
+ mockUser.isModerator = false;
+
+ return modController.endHandler(command).then(() => {
+ //Output back to mod
+ view.reportError.calledWith(command).should.be.true;
+ const modOutput = view.reportError.getCall(0).args[2].toString();
+ modOutput.should.include('You are not a moderator');
+ });
+ });
+
+ it('Should reject lack of winner', () => {
+ const command = {
+ getTopic: () => Promise.resolve({
+ id: 12345
+ }),
+ getUser: () => Promise.resolve({
+ username: 'tehNinja'
+ }),
+ args: [],
+ parent: {
+ ids: {
+ topic: 12345
+ }
+ },
+ };
+
+ return modController.endHandler(command).then(() => {
+ //Output back to mod
+ view.reportError.calledWith(command).should.be.true;
+ });
+ });
+
+ it('Should reject bad winner', () => {
+ const command = {
+ getTopic: () => Promise.resolve({
+ id: 12345
+ }),
+ getUser: () => Promise.resolve({
+ username: 'tehNinja'
+ }),
+ args: ['banana'],
+ parent: {
+ ids: {
+ topic: 12345
+ }
+ },
+ };
+
+ return modController.endHandler(command).then(() => {
+ //Output back to mod
+ view.reportError.calledWith(command).should.be.true;
+ });
+ });
+
+ it('Should reject non-existant game', () => {
+ const command = {
+ getTopic: () => Promise.resolve({
+ id: 12345
+ }),
+ getUser: () => Promise.resolve({
+ username: 'tehNinja'
+ }),
+ args: ['scum'],
+ parent: {
+ ids: {
+ topic: 12345
+ }
+ },
+ };
+
+ sandbox.stub(mockdao, 'getGameByTopicId').rejects('No such game');
+ sandbox.spy(mockGame, 'nextPhase');
+
+ return modController.endHandler(command).then(() => {
+ //Output back to mod
+ view.reportError.calledWith(command).should.be.true;
+ const modOutput = view.reportError.getCall(0).args[2];
+ modOutput.should.be.an('Error');
+ modOutput.toString().should.include('Error: No such game');
+ });
+ });
+
+ it('Should set the game inactive', () => {
+ const command = {
+ getTopic: () => Promise.resolve({
+ id: 12345
+ }),
+ getUser: () => Promise.resolve({
+ username: 'tehNinja'
+ }),
+ args: ['scum'],
+ parent: {
+ ids: {
+ topic: 12345
+ }
+ },
+ };
+
+ sandbox.spy(mockGame, 'setActive');
+
+ return modController.endHandler(command).then(() => {
+ mockGame.setActive.should.have.been.calledWith(false);
+ });
+ });
+
+ it('Should set the winner', () => {
+ const command = {
+ getTopic: () => Promise.resolve({
+ id: 12345
+ }),
+ getUser: () => Promise.resolve({
+ username: 'tehNinja'
+ }),
+ args: ['scum'],
+ parent: {
+ ids: {
+ topic: 12345
+ }
+ },
+ };
+
+ sandbox.spy(mockGame, 'setValue');
+
+ return modController.endHandler(command).then(() => {
+ mockGame.setValue.should.have.been.calledWith('winner', 'scum');
+ });
+ });
+
+ it('Should send stats', () => {
+ const command = {
+ getTopic: () => Promise.resolve({
+ id: 12345
+ }),
+ getUser: () => Promise.resolve({
+ username: 'tehNinja'
+ }),
+ args: ['scum'],
+ parent: {
+ ids: {
+ topic: 12345
+ }
+ },
+ };
+
+ const fakeData = {
+ some: 'keys',
+ are: 'included'
+ };
+
+ sandbox.stub(mockGame, 'toJSON').returns(fakeData);
+
+ return modController.endHandler(command).then(() => {
+ mockGame.toJSON.called.should.be.true;
+ mockRequest.should.have.been.calledWith('http://mafia.sockdrawer.io:5984/games', fakeData);
+ });
+ });
+ });
});