diff --git a/docs/documentation/api/Lobby.md b/docs/documentation/api/Lobby.md index c3dcd42ee..665fa3466 100644 --- a/docs/documentation/api/Lobby.md +++ b/docs/documentation/api/Lobby.md @@ -44,7 +44,6 @@ Options are: - `apiPort`: If specified, it runs the Lobby API in a separate Koa server on this port. Otherwise, it shares the same Koa server runnning on the default boardgame.io `port`. - `apiCallback`: Called when the Koa server is ready. Only applicable if `apiPort` is specified. -- `uuid`: Function that returns an unique identifier, needed for creating new game ID codes. If not specified, uses [shortid](https://www.npmjs.com/package/shortid). #### Creating a room diff --git a/docs/documentation/api/Server.md b/docs/documentation/api/Server.md index c6b90b74f..514de523b 100644 --- a/docs/documentation/api/Server.md +++ b/docs/documentation/api/Server.md @@ -24,10 +24,12 @@ A config object with the following options: 3. `transport` (_object_): the transport implementation. If not provided, socket.io is used. - -4. `generateCredentials` (_function_): an optional function that returns player credentials to store in the game metadata and validate against. If not specified, the Lobby’s `uuid` implementation will be used. -5. `authenticateCredentials` (_function_): an optional function that tests if a player’s move is made with the correct credentials when using the default socket.io transport implementation. +4. `uuid` (_function_): an optional function that returns a unique identifier, used to create new game IDs and — if `generateCredentials` is not specified — player credentials. Defaults to [shortid](https://www.npmjs.com/package/shortid). + +5. `generateCredentials` (_function_): an optional function that returns player credentials to store in the game metadata and validate against. If not specified, the `uuid` function will be used. + +6. `authenticateCredentials` (_function_): an optional function that tests if a player’s move is made with the correct credentials when using the default socket.io transport implementation. ### Returns @@ -39,6 +41,7 @@ An object that contains: _({ apiServer, appServer }) => {}_ 3. app (_object_): The Koa app. 4. db (_object_): The `db` implementation. +5. router (_object_): The Koa Router for the server API. ### Usage diff --git a/src/server/api.test.ts b/src/server/api.test.ts index c64aaf239..9123699c6 100644 --- a/src/server/api.test.ts +++ b/src/server/api.test.ts @@ -9,7 +9,7 @@ import request from 'supertest'; import Koa from 'koa'; -import { addApiToServer, createApiServer } from './api'; +import { createRouter, configureApp } from './api'; import { ProcessGameConfig } from '../core/game'; import * as StorageAPI from './db/base'; import { Game } from '../types'; @@ -63,7 +63,21 @@ class AsyncStorage extends StorageAPI.Async { } } -describe('.createApiServer', () => { +describe('.createRouter', () => { + function addApiToServer({ + app, + ...args + }: { app: Koa } & Parameters[0]) { + const router = createRouter(args); + configureApp(app, router); + } + + function createApiServer(args: Parameters[0]) { + const app = new Koa(); + addApiToServer({ app, ...args }); + return app; + } + describe('creating a game', () => { let response; let app: Koa; @@ -90,8 +104,7 @@ describe('.createApiServer', () => { delete process.env.API_SECRET; const uuid = () => 'gameID'; - const lobbyConfig = { uuid }; - app = createApiServer({ db, games, lobbyConfig }); + app = createApiServer({ db, games, uuid }); response = await request(app.callback()) .post('/games/foo/create') @@ -306,9 +319,7 @@ describe('.createApiServer', () => { const app = createApiServer({ db, games, - lobbyConfig: { - uuid: () => 'gameID', - }, + uuid: () => 'gameID', generateCredentials: () => credentials, }); response = await request(app.callback()) @@ -1027,10 +1038,8 @@ describe('.createApiServer', () => { }); test('creates new game data', async () => { - const lobbyConfig = { - uuid: () => 'newGameID', - }; - const app = createApiServer({ db, games, lobbyConfig }); + const uuid = () => 'newGameID'; + const app = createApiServer({ db, games, uuid }); response = await request(app.callback()) .post('/games/foo/1/playAgain') .send('playerID=0&credentials=SECRET1&numPlayers=4'); @@ -1244,9 +1253,7 @@ describe('.createApiServer', () => { }); }); }); -}); -describe('.addApiToServer', () => { describe('when server app is provided', () => { let db: AsyncStorage; let server; @@ -1272,7 +1279,7 @@ describe('.addApiToServer', () => { test('call .use method several times with uuid', async () => { const uuid = () => 'foo'; - addApiToServer({ app: server, db, games, lobbyConfig: { uuid } }); + addApiToServer({ app: server, db, games, uuid }); expect(server.use.mock.calls.length).toBeGreaterThan(1); }); }); diff --git a/src/server/api.ts b/src/server/api.ts index 9b8141cec..378639add 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -9,7 +9,7 @@ import Koa from 'koa'; import Router from 'koa-router'; import koaBody from 'koa-body'; -import { generate as uuid } from 'shortid'; +import { generate as shortid } from 'shortid'; import cors from '@koa/cors'; import { InitializeGame } from '../core/initialize'; @@ -27,14 +27,21 @@ import { Server, Game } from '../types'; * @param {object } lobbyConfig - Configuration options for the lobby. * @param {boolean} unlisted - Whether the game should be excluded from public listing. */ -export const CreateGame = async ( - db: StorageAPI.Sync | StorageAPI.Async, - game: Game, - numPlayers: number, - setupData: any, - lobbyConfig: Server.LobbyConfig, - unlisted: boolean -) => { +export const CreateGame = async ({ + db, + game, + numPlayers, + setupData, + uuid, + unlisted, +}: { + db: StorageAPI.Sync | StorageAPI.Async; + game: Game; + numPlayers: number; + setupData: any; + uuid: () => string; + unlisted: boolean; +}) => { if (!numPlayers || typeof numPlayers !== 'number') numPlayers = 2; const metadata: Server.GameMetadata = { @@ -47,7 +54,7 @@ export const CreateGame = async ( metadata.players[playerIndex] = { id: playerIndex }; } - const gameID = lobbyConfig.uuid(); + const gameID = uuid(); const initialState = InitializeGame({ game, numPlayers, setupData }); await db.createGame(gameID, { metadata, initialState }); @@ -55,40 +62,19 @@ export const CreateGame = async ( return gameID; }; -export const createApiServer = ({ +export const createRouter = ({ db, games, - lobbyConfig, + uuid, generateCredentials, }: { - db: StorageAPI.Sync | StorageAPI.Async; games: Game[]; - lobbyConfig?: Server.LobbyConfig; - generateCredentials?: Server.GenerateCredentials; -}) => { - const app = new Koa(); - return addApiToServer({ app, db, games, lobbyConfig, generateCredentials }); -}; - -export const addApiToServer = ({ - app, - db, - games, - lobbyConfig, - generateCredentials, -}: { - app: Koa; - games: Game[]; - lobbyConfig?: Server.LobbyConfig; + uuid?: () => string; generateCredentials?: Server.GenerateCredentials; db: StorageAPI.Sync | StorageAPI.Async; -}) => { - if (!lobbyConfig) lobbyConfig = {}; - lobbyConfig = { - ...lobbyConfig, - uuid: lobbyConfig.uuid || uuid, - generateCredentials: generateCredentials || lobbyConfig.uuid || uuid, - }; +}): Router => { + uuid = uuid || shortid; + generateCredentials = generateCredentials || uuid; const router = new Router(); router.get('/games', async ctx => { @@ -108,14 +94,14 @@ export const addApiToServer = ({ const game = games.find(g => g.name === gameName); if (!game) ctx.throw(404, 'Game ' + gameName + ' not found'); - const gameID = await CreateGame( + const gameID = await CreateGame({ db, game, numPlayers, setupData, - lobbyConfig, - unlisted - ); + uuid, + unlisted, + }); ctx.body = { gameID, @@ -194,7 +180,7 @@ export const addApiToServer = ({ metadata.players[playerID].data = data; } metadata.players[playerID].name = playerName; - const playerCredentials = await lobbyConfig.generateCredentials(ctx); + const playerCredentials = await generateCredentials(ctx); metadata.players[playerID].credentials = playerCredentials; await db.setMetadata(gameID, metadata); @@ -271,14 +257,14 @@ export const addApiToServer = ({ } const game = games.find(g => g.name === gameName); - const nextRoomID = await CreateGame( + const nextRoomID = await CreateGame({ db, game, numPlayers, setupData, - lobbyConfig, - unlisted - ); + uuid, + unlisted, + }); metadata.nextRoomID = nextRoomID; await db.setMetadata(gameID, metadata); @@ -335,6 +321,10 @@ export const addApiToServer = ({ router.post('/games/:name/:id/update', koaBody(), updatePlayerMetadata); + return router; +}; + +export const configureApp = (app: Koa, router: Router): void => { app.use(cors()); // If API_SECRET is set, then require that requests set an @@ -351,6 +341,4 @@ export const addApiToServer = ({ }); app.use(router.routes()).use(router.allowedMethods()); - - return app; }; diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 14f0ff757..a5eff8ac5 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -7,7 +7,6 @@ */ import { Server, createServerRunConfig, KoaServer } from '.'; -import * as api from './api'; import { StorageAPI } from '../types'; const game = { seed: 0 }; @@ -17,20 +16,6 @@ jest.mock('../core/logger', () => ({ error: () => {}, })); -const mockApiServerListen = jest.fn((port, listeningCallback?: () => void) => { - if (listeningCallback) listeningCallback(); - return { - address: () => ({ port: 'mock-api-port' }), - close: () => {}, - }; -}); -jest.mock('./api', () => ({ - createApiServer: jest.fn(() => ({ - listen: mockApiServerListen, - })), - addApiToServer: jest.fn(), -})); - jest.mock('koa-socket-2', () => { class MockSocket { on() {} @@ -56,6 +41,7 @@ jest.mock('koa', () => { return class { constructor() { (this as any).context = {}; + (this as any).use = () => this; (this as any).callback = () => {}; (this as any).listen = (port, listeningCallback?: () => void) => { if (listeningCallback) listeningCallback(); @@ -98,9 +84,6 @@ describe('run', () => { beforeEach(() => { server = null; runningServer = null; - (api.createApiServer as jest.Mock).mockClear(); - (api.addApiToServer as jest.Mock).mockClear(); - (mockApiServerListen as jest.Mock).mockClear(); }); afterEach(() => { @@ -115,9 +98,8 @@ describe('run', () => { runningServer = await server.run(undefined); expect(server).not.toBeUndefined(); - expect(api.addApiToServer).toBeCalled(); - expect(api.createApiServer).not.toBeCalled(); - expect(mockApiServerListen).not.toBeCalled(); + expect(runningServer.appServer).not.toBeUndefined(); + expect(runningServer.apiServer).toBeUndefined(); }); test('multiple servers running', async () => { @@ -128,9 +110,8 @@ describe('run', () => { }); expect(server).not.toBeUndefined(); - expect(api.addApiToServer).not.toBeCalled(); - expect(api.createApiServer).toBeCalled(); - expect(mockApiServerListen).toBeCalled(); + expect(runningServer.appServer).not.toBeUndefined(); + expect(runningServer.apiServer).not.toBeUndefined(); }); }); diff --git a/src/server/index.ts b/src/server/index.ts index 83aa0c728..7f8d87679 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -8,7 +8,7 @@ import Koa from 'koa'; -import { addApiToServer, createApiServer } from './api'; +import { createRouter, configureApp } from './api'; import { DBFromEnv } from './db'; import { ProcessGameConfig } from '../core/game'; import * as logger from '../core/logger'; @@ -62,6 +62,7 @@ interface ServerOpts { games: Game[]; db?: StorageAPI.Async | StorageAPI.Sync; transport?; + uuid?: () => string; authenticateCredentials?: ServerTypes.AuthenticateCredentials; generateCredentials?: ServerTypes.GenerateCredentials; https?: HttpsOptions; @@ -76,6 +77,7 @@ interface ServerOpts { * @param authenticateCredentials - Function to test player credentials. * @param generateCredentials - Method for API to generate player credentials. * @param https - HTTPS configuration options passed through to the TLS module. + * @param lobbyConfig - Configuration options for the Lobby API server. */ export function Server({ games, @@ -84,6 +86,7 @@ export function Server({ authenticateCredentials, generateCredentials, https, + uuid, }: ServerOpts) { const app = new Koa(); @@ -106,29 +109,28 @@ export function Server({ } transport.init(app, games); + const router = createRouter({ db, games, uuid, generateCredentials }); + return { app, db, + router, - run: async (portOrConfig: number | object, callback?: () => void) => { + run: async (portOrConfig: number | ServerConfig, callback?: () => void) => { const serverRunConfig = createServerRunConfig(portOrConfig, callback); // DB await db.connect(); // Lobby API - const lobbyConfig: ServerTypes.LobbyConfig = serverRunConfig.lobbyConfig; + const lobbyConfig = serverRunConfig.lobbyConfig; let apiServer: KoaServer | undefined; if (!lobbyConfig || !lobbyConfig.apiPort) { - addApiToServer({ app, db, games, lobbyConfig, generateCredentials }); + configureApp(app, router); } else { // Run API in a separate Koa app. - const api = createApiServer({ - db, - games, - lobbyConfig, - generateCredentials, - }); + const api = new Koa(); + configureApp(api, router); await new Promise(resolve => { apiServer = api.listen(lobbyConfig.apiPort, resolve); }); diff --git a/src/types.ts b/src/types.ts index d8badcfd6..616dfdf38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -284,13 +284,6 @@ export namespace Server { nextRoomID?: string; unlisted?: boolean; } - - export interface LobbyConfig { - uuid?: () => string; - generateCredentials?: GenerateCredentials; - apiPort?: number; - apiCallback?: () => void; - } } export namespace CredentialedActionShape {