boardgame.io
Version:
library for turn-based games
1,336 lines (1,199 loc) • 41.1 kB
text/typescript
/*
* Copyright 2018 The boardgame.io Authors
*
* Use of this source code is governed by a MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
import request from 'supertest';
import Koa from 'koa';
import { addApiToServer, createApiServer } from './api';
import { ProcessGameConfig } from '../core/game';
import * as StorageAPI from './db/base';
import { Game } from '../types';
jest.setTimeout(2000000000);
type StorageMocks = Record<
'createGame' | 'setState' | 'fetch' | 'setMetadata' | 'listGames' | 'wipe',
jest.Mock | ((...args: any[]) => any)
>;
class AsyncStorage extends StorageAPI.Async {
public mocks: StorageMocks;
constructor(args: Partial<StorageMocks> = {}) {
super();
this.mocks = {
createGame: args.createGame || jest.fn(),
setState: args.setState || jest.fn(),
fetch: args.fetch || jest.fn(() => ({})),
setMetadata: args.setMetadata || jest.fn(),
listGames: args.listGames || jest.fn(() => []),
wipe: args.wipe || jest.fn(),
};
}
async connect() {}
async createGame(...args) {
this.mocks.createGame(...args);
}
async fetch(...args) {
return this.mocks.fetch(...args);
}
async setState(...args) {
this.mocks.setState(...args);
}
async setMetadata(...args) {
this.mocks.setMetadata(...args);
}
async wipe(...args) {
this.mocks.wipe(...args);
}
async listGames(...args) {
return this.mocks.listGames(...args);
}
}
describe('.createApiServer', () => {
describe('creating a game', () => {
let response;
let app: Koa;
let db: AsyncStorage;
let games: Game[];
beforeEach(async () => {
db = new AsyncStorage();
games = [
{
name: 'foo',
setup: (_, setupData) =>
setupData
? {
colors: setupData.colors,
}
: {},
},
];
});
describe('for an unprotected lobby server', () => {
beforeEach(async () => {
delete process.env.API_SECRET;
const uuid = () => 'gameID';
const lobbyConfig = { uuid };
app = createApiServer({ db, games, lobbyConfig });
response = await request(app.callback())
.post('/games/foo/create')
.send({ numPlayers: 3 });
});
test('is successful', () => {
expect(response.status).toEqual(200);
});
test('creates game state and metadata', () => {
expect(db.mocks.createGame).toHaveBeenCalledWith(
'gameID',
expect.objectContaining({
initialState: expect.objectContaining({
ctx: expect.objectContaining({
numPlayers: 3,
}),
}),
metadata: expect.objectContaining({
gameName: 'foo',
players: expect.objectContaining({
'0': expect.objectContaining({}),
'1': expect.objectContaining({}),
}),
unlisted: false,
}),
})
);
});
test('returns game id', () => {
expect(response.body.gameID).not.toBeNull();
});
describe('without numPlayers', () => {
beforeEach(async () => {
response = await request(app.callback()).post('/games/foo/create');
});
test('uses default numPlayers', () => {
expect(db.mocks.createGame).toHaveBeenCalledWith(
'gameID',
expect.objectContaining({
initialState: expect.objectContaining({
ctx: expect.objectContaining({
numPlayers: 2,
}),
}),
})
);
});
});
describe('for an unknown game name', () => {
beforeEach(async () => {
response = await request(app.callback()).post('/games/bar/create');
});
test('returns 404 error', () => {
expect(response.status).toEqual(404);
});
});
describe('with setupData', () => {
beforeEach(async () => {
response = await request(app.callback())
.post('/games/foo/create')
.send({
setupData: {
colors: {
'0': 'green',
'1': 'red',
},
},
});
});
test('includes setupData in metadata', () => {
expect(db.mocks.createGame).toHaveBeenCalledWith(
'gameID',
expect.objectContaining({
metadata: expect.objectContaining({
setupData: expect.objectContaining({
colors: expect.objectContaining({
'0': 'green',
'1': 'red',
}),
}),
}),
})
);
});
test('passes setupData to game setup function', () => {
expect(db.mocks.createGame).toHaveBeenCalledWith(
'gameID',
expect.objectContaining({
initialState: expect.objectContaining({
G: expect.objectContaining({
colors: {
'0': 'green',
'1': 'red',
},
}),
}),
})
);
});
});
describe('with unlisted option', () => {
beforeEach(async () => {
response = await request(app.callback())
.post('/games/foo/create')
.send({ unlisted: true });
});
test('sets unlisted in metadata', () => {
expect(db.mocks.createGame).toHaveBeenCalledWith(
'gameID',
expect.objectContaining({
metadata: expect.objectContaining({
unlisted: true,
}),
})
);
});
});
});
describe('for a protected lobby', () => {
beforeEach(() => {
process.env.API_SECRET = 'protected';
app = createApiServer({ db, games });
});
describe('without the lobby token', () => {
beforeEach(async () => {
response = await request(app.callback()).post('/games/foo/create');
});
test('fails', () => {
expect(response.status).toEqual(403);
});
});
describe('with the lobby token', () => {
beforeEach(async () => {
response = await request(app.callback())
.post('/games/foo/create')
.set('API-Secret', 'protected');
});
test('succeeds', () => {
expect(response.status).toEqual(200);
});
});
});
});
describe('joining a room', () => {
let response;
let db: AsyncStorage;
let games: Game[];
let credentials: string;
beforeEach(() => {
credentials = 'SECRET';
games = [ProcessGameConfig({ name: 'foo' })];
});
describe('for an unprotected lobby', () => {
beforeEach(() => {
delete process.env.API_SECRET;
});
describe('when the game does not exist', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: () => ({ metadata: null }),
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/join')
.send('playerID=0&playerName=alice');
});
test('throws a "not found" error', async () => {
expect(response.status).toEqual(404);
});
});
describe('when the game does exist', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {},
},
},
};
},
});
});
describe('when the playerID is available', () => {
beforeEach(async () => {
const app = createApiServer({
db,
games,
lobbyConfig: {
uuid: () => 'gameID',
},
generateCredentials: () => credentials,
});
response = await request(app.callback())
.post('/games/foo/1/join')
.send({ playerID: 0, playerName: 'alice' });
});
test('is successful', async () => {
expect(response.status).toEqual(200);
});
test('returns the player credentials', async () => {
expect(response.body.playerCredentials).toEqual(credentials);
});
test('updates the player name', async () => {
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
'1',
expect.objectContaining({
players: expect.objectContaining({
'0': expect.objectContaining({
name: 'alice',
}),
}),
})
);
});
describe('when custom data is provided', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/join')
.send({ playerID: 0, playerName: 'alice', data: 99 });
});
test('updates the player data', async () => {
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
'1',
expect.objectContaining({
players: expect.objectContaining({
'0': expect.objectContaining({
data: 99,
}),
}),
})
);
});
});
});
describe('when the playerID does not exist', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/join')
.send('playerID=1&playerName=alice');
});
test('throws error 404', async () => {
expect(response.status).toEqual(404);
});
});
describe('when playerID is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/join')
.send('playerName=1');
});
test('throws error 403', async () => {
expect(response.status).toEqual(403);
});
});
describe('when playerName is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/join')
.send('playerID=1');
});
test('throws error 403', async () => {
expect(response.status).toEqual(403);
});
});
describe('when the playerID is not available', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
credentials,
name: 'bob',
},
},
},
};
},
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/join')
.send('playerID=0&playerName=alice');
});
test('throws error 409', async () => {
expect(response.status).toEqual(409);
});
});
});
});
});
describe('rename with deprecated endpoint', () => {
let response;
let db: AsyncStorage;
let games: Game[];
const warnMsg =
'This endpoint /rename is deprecated. Please use /update instead.';
beforeEach(() => {
games = [ProcessGameConfig({ name: 'foo' })];
console.warn = jest.fn();
});
describe('for an unprotected lobby', () => {
beforeEach(() => {
delete process.env.API_SECRET;
});
describe('when the game does not exist', () => {
test('throws a "not found" error', async () => {
db = new AsyncStorage({
fetch: async () => ({ metadata: null }),
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/rename')
.send('playerID=0&playerName=alice&newName=ali');
expect(response.status).toEqual(404);
expect(console.warn).toBeCalledWith(warnMsg);
});
});
describe('when the game does exist', () => {
describe('when the playerID does exist', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
name: 'alice',
credentials: 'SECRET1',
},
'1': {
name: 'bob',
credentials: 'SECRET2',
},
},
},
};
},
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/rename')
.send('playerID=0&credentials=SECRET1&newName=ali');
});
describe('when the playerName is not a string', () => {
test('throws newName must be a string', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/rename')
.send({ playerID: 0, credentials: 'SECRET1', newName: 2 });
expect(response.text).toEqual(
'newName must be a string, got number'
);
expect(console.warn).toBeCalledWith(warnMsg);
});
});
test('is successful', async () => {
expect(response.status).toEqual(200);
expect(console.warn).toBeCalledWith(warnMsg);
});
test('updates the players', async () => {
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
'1',
expect.objectContaining({
players: expect.objectContaining({
'0': expect.objectContaining({
name: 'ali',
}),
}),
})
);
expect(console.warn).toBeCalledWith(warnMsg);
});
});
describe('when the playerID does not exist', () => {
test('throws error 404', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/rename')
.send('playerID=2&credentials=SECRET1&newName=joe');
expect(response.status).toEqual(404);
expect(console.warn).toBeCalledWith(warnMsg);
});
});
describe('when the credentials are invalid', () => {
test('throws error 404', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/rename')
.send('playerID=0&credentials=SECRET2&newName=mike');
expect(response.status).toEqual(403);
expect(console.warn).toBeCalledWith(warnMsg);
});
});
describe('when playerID is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/rename')
.send('credentials=foo&newName=bill');
});
test('throws error 403', async () => {
expect(response.status).toEqual(403);
expect(console.warn).toBeCalledWith(warnMsg);
});
});
describe('when newName is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/rename')
.send('credentials=foo&playerID=0');
});
test('throws error 403', async () => {
expect(response.status).toEqual(403);
expect(console.warn).toBeCalledWith(warnMsg);
});
});
});
});
});
describe('rename with update endpoint', () => {
let response;
let db: AsyncStorage;
let games: Game[];
beforeEach(() => {
games = [ProcessGameConfig({ name: 'foo' })];
});
describe('for an unprotected lobby', () => {
beforeEach(() => {
delete process.env.API_SECRET;
});
describe('when the game does not exist', () => {
test('throws game not found', async () => {
db = new AsyncStorage({
fetch: async () => ({ metadata: null }),
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send('playerID=0&playerName=alice&newName=ali');
expect(response.text).toEqual('Game 1 not found');
});
});
describe('when the game does exist', () => {
describe('when the playerID does exist', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
name: 'alice',
credentials: 'SECRET1',
},
'1': {
name: 'bob',
credentials: 'SECRET2',
},
},
},
};
},
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send('playerID=0&credentials=SECRET1&newName=ali');
});
describe('when the playerName is not a string', () => {
test('throws newName must be a string', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send({ playerID: 0, credentials: 'SECRET1', newName: 2 });
expect(response.text).toEqual(
'newName must be a string, got number'
);
});
});
test('is successful', async () => {
expect(response.status).toEqual(200);
});
test('updates the players', async () => {
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
'1',
expect.objectContaining({
players: expect.objectContaining({
'0': expect.objectContaining({
name: 'ali',
}),
}),
})
);
});
});
describe('when the playerID does not exist', () => {
test('throws player not found', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send('playerID=2&credentials=SECRET1&newName=joe');
expect(response.text).toEqual('Player 2 not found');
});
});
describe('when the credentials are invalid', () => {
test('throws invalid credentials', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send('playerID=0&credentials=SECRET2&newName=mike');
expect(response.text).toEqual('Invalid credentials SECRET2');
});
});
describe('when playerID is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send('credentials=foo&newName=bill');
});
test('throws playerID is required', async () => {
expect(response.text).toEqual('playerID is required');
});
});
describe('when newName is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send('credentials=foo&playerID=0');
});
test('throws newName is required', async () => {
expect(response.text).toEqual('newName or data is required');
});
});
});
});
});
describe('updating player metadata', () => {
let response;
let db: AsyncStorage;
let games: Game[];
beforeEach(() => {
games = [ProcessGameConfig({ name: 'foo' })];
});
describe('for an unprotected lobby', () => {
beforeEach(() => {
delete process.env.API_SECRET;
});
describe('when the game does not exist', () => {
test('throws game not found', async () => {
db = new AsyncStorage({
fetch: async () => ({ metadata: null }),
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send({ playerID: 0, data: { subdata: 'text' } });
expect(response.text).toEqual('Game 1 not found');
});
});
describe('when the game does exist', () => {
describe('when the playerID does exist', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
name: 'alice',
credentials: 'SECRET1',
},
'1': {
name: 'bob',
credentials: 'SECRET2',
},
},
},
};
},
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send({
playerID: 0,
credentials: 'SECRET1',
data: { subdata: 'text' },
});
});
test('is successful', async () => {
expect(response.status).toEqual(200);
});
test('updates the players', async () => {
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
'1',
expect.objectContaining({
players: expect.objectContaining({
'0': expect.objectContaining({
data: expect.objectContaining({
subdata: 'text',
}),
}),
}),
})
);
});
});
describe('when the playerID does not exist', () => {
test('throws playerID not found', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send({
playerID: 2,
credentials: 'SECRET1',
data: { subdata: 'text' },
});
expect(response.text).toEqual('Player 2 not found');
});
});
describe('when the credentials are invalid', () => {
test('invalid credentials', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send({
playerID: 0,
credentials: 'SECRET2',
data: { subdata: 'text' },
});
expect(response.text).toEqual('Invalid credentials SECRET2');
});
});
describe('when playerID is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send({ credentials: 'foo', data: { subdata: 'text' } });
});
test('throws playerID is required', async () => {
expect(response.text).toEqual('playerID is required');
});
});
describe('when data is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/update')
.send({ playerID: 0, credentials: 'foo' });
});
test('throws data is required', async () => {
expect(response.text).toEqual('newName or data is required');
});
});
});
});
});
describe('leaving a room', () => {
let response;
let db: AsyncStorage;
let games: Game[];
beforeEach(() => {
games = [ProcessGameConfig({ name: 'foo' })];
});
describe('for an unprotected lobby', () => {
beforeEach(() => {
delete process.env.API_SECRET;
});
describe('when the game does not exist', () => {
test('throws a "not found" error', async () => {
db = new AsyncStorage({
fetch: async () => ({ metadata: null }),
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/leave')
.send('playerID=0&playerName=alice');
expect(response.status).toEqual(404);
});
});
describe('when the game does exist', () => {
describe('when the playerID does exist', () => {
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
name: 'alice',
credentials: 'SECRET1',
},
'1': {
name: 'bob',
credentials: 'SECRET2',
},
},
},
};
},
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/leave')
.send('playerID=0&credentials=SECRET1');
});
test('is successful', async () => {
expect(response.status).toEqual(200);
});
test('updates the players', async () => {
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
'1',
expect.objectContaining({
players: expect.objectContaining({
'0': expect.objectContaining({}),
'1': expect.objectContaining({
name: 'bob',
credentials: 'SECRET2',
}),
}),
})
);
});
describe('when there are not players left', () => {
test('removes the game', async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
name: 'alice',
credentials: 'SECRET1',
},
'1': {
credentials: 'SECRET2',
},
},
},
};
},
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/leave')
.send('playerID=0&credentials=SECRET1');
expect(db.mocks.wipe).toHaveBeenCalledWith('1');
});
});
});
describe('when the playerID does not exist', () => {
test('throws error 404', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/leave')
.send('playerID=2&credentials=SECRET1');
expect(response.status).toEqual(404);
});
});
describe('when the credentials are invalid', () => {
test('throws error 404', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/leave')
.send('playerID=0&credentials=SECRET2');
expect(response.status).toEqual(403);
});
});
describe('when playerID is omitted', () => {
beforeEach(async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/leave')
.send('credentials=foo');
});
test('throws error 403', async () => {
expect(response.status).toEqual(403);
});
});
});
});
});
describe('requesting game list', () => {
let db: AsyncStorage;
beforeEach(() => {
delete process.env.API_SECRET;
db = new AsyncStorage();
});
describe('when given 2 games', () => {
let response;
beforeEach(async () => {
let games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
let app = createApiServer({ db, games });
response = await request(app.callback()).get('/games');
});
test('should get 2 games', async () => {
expect(JSON.parse(response.text)).toEqual(['foo', 'bar']);
});
});
});
describe('play again', () => {
let response;
let db: AsyncStorage;
let games: Game[];
beforeEach(() => {
games = [ProcessGameConfig({ name: 'foo' })];
delete process.env.API_SECRET;
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
setupData: {
colors: {
'0': 'green',
'1': 'red',
},
},
players: {
'0': {
name: 'alice',
credentials: 'SECRET1',
},
'1': {
name: 'bob',
credentials: 'SECRET2',
},
'2': {
name: 'chris',
credentials: 'SECRET3',
},
},
},
};
},
});
});
test('creates new game data', async () => {
const lobbyConfig = {
uuid: () => 'newGameID',
};
const app = createApiServer({ db, games, lobbyConfig });
response = await request(app.callback())
.post('/games/foo/1/playAgain')
.send({
playerID: 0,
credentials: 'SECRET1',
numPlayers: 4,
setupData: {
colors: {
'3': 'blue',
},
},
});
expect(db.mocks.createGame).toHaveBeenCalledWith(
'newGameID',
expect.objectContaining({
initialState: expect.objectContaining({
ctx: expect.objectContaining({
numPlayers: 4,
}),
}),
metadata: expect.objectContaining({
setupData: expect.objectContaining({
colors: expect.objectContaining({
'3': 'blue',
}),
}),
}),
})
);
expect(response.body.nextRoomID).toBe('newGameID');
});
test('when game configuration not supplied, uses previous game config', async () => {
const lobbyConfig = {
uuid: () => 'newGameID',
};
const app = createApiServer({ db, games, lobbyConfig });
response = await request(app.callback())
.post('/games/foo/1/playAgain')
.send('playerID=0&credentials=SECRET1');
expect(db.mocks.createGame).toHaveBeenCalledWith(
'newGameID',
expect.objectContaining({
initialState: expect.objectContaining({
ctx: expect.objectContaining({
numPlayers: 3,
}),
}),
metadata: expect.objectContaining({
setupData: expect.objectContaining({
colors: expect.objectContaining({
'0': 'green',
'1': 'red',
}),
}),
}),
})
);
expect(response.body.nextRoomID).toBe('newGameID');
});
test('fetches next id', async () => {
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
name: 'alice',
credentials: 'SECRET1',
},
'1': {
name: 'bob',
credentials: 'SECRET2',
},
},
nextRoomID: '12345',
},
};
},
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/playAgain')
.send('playerID=0&credentials=SECRET1');
expect(response.body.nextRoomID).toBe('12345');
});
test('when the game does not exist throws a "not found" error', async () => {
db = new AsyncStorage({
fetch: async () => ({ metadata: null }),
});
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/playAgain')
.send('playerID=0&playerName=alice');
expect(response.status).toEqual(404);
});
test('when the playerID is undefined throws error 403', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/playAgain')
.send('credentials=SECRET1');
expect(response.status).toEqual(403);
});
test('when the playerID does not exist throws error 404', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/playAgain')
.send('playerID=3&credentials=SECRET1');
expect(response.status).toEqual(404);
});
test('when the credentials are invalid throws error 404', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/playAgain')
.send('playerID=0&credentials=SECRET2');
expect(response.status).toEqual(403);
});
test('when playerID is omitted throws error 403', async () => {
const app = createApiServer({ db, games });
response = await request(app.callback())
.post('/games/foo/1/leave')
.send('credentials=foo');
expect(response.status).toEqual(403);
});
});
describe('requesting room list', () => {
let db: AsyncStorage;
beforeEach(() => {
delete process.env.API_SECRET;
db = new AsyncStorage({
fetch: async gameID => {
return {
metadata: {
players: {
'0': {
id: 0,
credentials: 'SECRET1',
},
'1': {
id: 1,
credentials: 'SECRET2',
},
},
unlisted: gameID === 'bar-4',
},
};
},
listGames: async opts => {
const metadata = {
'foo-0': { gameName: 'foo' },
'foo-1': { gameName: 'foo' },
'bar-2': { gameName: 'bar' },
'bar-3': { gameName: 'bar' },
'bar-4': { gameName: 'bar' },
};
const keys = Object.keys(metadata);
if (opts && opts.gameName) {
return keys.filter(key => metadata[key].gameName === opts.gameName);
}
return [...keys];
},
});
});
describe('when given 2 rooms', () => {
let response;
let rooms;
beforeEach(async () => {
let games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
let app = createApiServer({ db, games });
response = await request(app.callback()).get('/games/bar');
rooms = JSON.parse(response.text).rooms;
});
test('returns rooms for the selected game', async () => {
expect(rooms).toHaveLength(2);
});
test('returns room ids', async () => {
expect(rooms[0].gameID).toEqual('bar-2');
expect(rooms[1].gameID).toEqual('bar-3');
});
test('returns player names', async () => {
expect(rooms[0].players).toEqual([{ id: 0 }, { id: 1 }]);
expect(rooms[1].players).toEqual([{ id: 0 }, { id: 1 }]);
});
});
});
describe('requesting room', () => {
let db: AsyncStorage;
beforeEach(() => {
delete process.env.API_SECRET;
db = new AsyncStorage({
fetch: async () => {
return {
metadata: {
players: {
'0': {
id: 0,
credentials: 'SECRET1',
},
'1': {
id: 1,
credentials: 'SECRET2',
},
},
},
};
},
listGames: async () => {
return ['bar:bar-0', 'foo:foo-0', 'bar:bar-1'];
},
});
});
describe('when given room ID', () => {
let response;
let room;
beforeEach(async () => {
let games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
let app = createApiServer({ db, games });
response = await request(app.callback()).get('/games/bar/bar-0');
room = JSON.parse(response.text);
});
test('returns game ids', async () => {
expect(room.roomID).toEqual('bar-0');
});
test('returns player names', async () => {
expect(room.players).toEqual([{ id: 0 }, { id: 1 }]);
});
});
describe('when given a non-existent room ID', () => {
let response;
beforeEach(async () => {
db = new AsyncStorage({
fetch: async () => ({ metadata: null }),
});
let games = [ProcessGameConfig({ name: 'foo' })];
let app = createApiServer({ db, games });
response = await request(app.callback()).get('/games/bar/doesnotexist');
});
test('throws error 404', async () => {
expect(response.status).toEqual(404);
});
});
});
});
describe('.addApiToServer', () => {
describe('when server app is provided', () => {
let db: AsyncStorage;
let server;
let useChain;
let games: Game[];
beforeEach(async () => {
useChain = jest.fn(() => ({ use: useChain }));
server = { use: useChain };
db = new AsyncStorage();
games = [
{
name: 'foo',
setup: () => {},
},
];
});
test('call .use method several times', async () => {
addApiToServer({ app: server, db, games });
expect(server.use.mock.calls.length).toBeGreaterThan(1);
});
test('call .use method several times with uuid', async () => {
const uuid = () => 'foo';
addApiToServer({ app: server, db, games, lobbyConfig: { uuid } });
expect(server.use.mock.calls.length).toBeGreaterThan(1);
});
});
});