Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): Expose API router #698

Merged
merged 6 commits into from May 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/documentation/api/Lobby.md
Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions docs/documentation/api/Server.md
Expand Up @@ -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

Expand All @@ -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

Expand Down
35 changes: 21 additions & 14 deletions src/server/api.test.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -63,7 +63,21 @@ class AsyncStorage extends StorageAPI.Async {
}
}

describe('.createApiServer', () => {
describe('.createRouter', () => {
function addApiToServer({
app,
...args
}: { app: Koa } & Parameters<typeof createRouter>[0]) {
const router = createRouter(args);
configureApp(app, router);
}

function createApiServer(args: Parameters<typeof createRouter>[0]) {
const app = new Koa();
addApiToServer({ app, ...args });
return app;
}

describe('creating a game', () => {
let response;
let app: Koa;
Expand All @@ -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')
Expand Down Expand Up @@ -306,9 +319,7 @@ describe('.createApiServer', () => {
const app = createApiServer({
db,
games,
lobbyConfig: {
uuid: () => 'gameID',
},
uuid: () => 'gameID',
generateCredentials: () => credentials,
});
response = await request(app.callback())
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -1244,9 +1253,7 @@ describe('.createApiServer', () => {
});
});
});
});

describe('.addApiToServer', () => {
describe('when server app is provided', () => {
let db: AsyncStorage;
let server;
Expand All @@ -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);
});
});
Expand Down
84 changes: 36 additions & 48 deletions src/server/api.ts
Expand Up @@ -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';
Expand All @@ -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 = {
Expand All @@ -47,48 +54,27 @@ 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 });

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 => {
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -351,6 +341,4 @@ export const addApiToServer = ({
});

app.use(router.routes()).use(router.allowedMethods());

return app;
};
29 changes: 5 additions & 24 deletions src/server/index.test.ts
Expand Up @@ -7,7 +7,6 @@
*/

import { Server, createServerRunConfig, KoaServer } from '.';
import * as api from './api';
import { StorageAPI } from '../types';

const game = { seed: 0 };
Expand All @@ -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() {}
Expand All @@ -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();
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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 () => {
Expand All @@ -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();
});
});

Expand Down