@mnbroatch/boardgame.io 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/ai/package.json +7 -0
- package/client/package.json +7 -0
- package/core/package.json +7 -0
- package/debug/package.json +7 -0
- package/dist/boardgameio.es.js +14238 -0
- package/dist/boardgameio.js +14277 -0
- package/dist/boardgameio.min.js +16 -0
- package/dist/cjs/Debug-9d141c06.js +9586 -0
- package/dist/cjs/ai-e0e8a768.js +377 -0
- package/dist/cjs/ai.js +20 -0
- package/dist/cjs/client-76dec77b.js +258 -0
- package/dist/cjs/client-a22d7500.js +524 -0
- package/dist/cjs/client.js +26 -0
- package/dist/cjs/core.js +52 -0
- package/dist/cjs/debug.js +18 -0
- package/dist/cjs/filter-player-view-bb02e2f6.js +89 -0
- package/dist/cjs/initialize-267fcd69.js +61 -0
- package/dist/cjs/internal.js +25 -0
- package/dist/cjs/master-2904879d.js +320 -0
- package/dist/cjs/master.js +18 -0
- package/dist/cjs/multiplayer.js +23 -0
- package/dist/cjs/plugin-random-7425844d.js +229 -0
- package/dist/cjs/plugins.js +59 -0
- package/dist/cjs/react-native.js +182 -0
- package/dist/cjs/react.js +727 -0
- package/dist/cjs/reducer-16eec232.js +1203 -0
- package/dist/cjs/server.js +4087 -0
- package/dist/cjs/socketio-7a0837eb.js +478 -0
- package/dist/cjs/testing.js +30 -0
- package/dist/cjs/transport-b1874dfa.js +37 -0
- package/dist/cjs/turn-order-b2ff8740.js +1136 -0
- package/dist/cjs/util-fcfd8fb8.js +140 -0
- package/dist/esm/Debug-0141fe2d.js +9577 -0
- package/dist/esm/ai-5c06e761.js +371 -0
- package/dist/esm/ai.js +8 -0
- package/dist/esm/client-2e653027.js +522 -0
- package/dist/esm/client-5f57c3f2.js +255 -0
- package/dist/esm/client.js +16 -0
- package/dist/esm/core.js +40 -0
- package/dist/esm/debug.js +10 -0
- package/dist/esm/filter-player-view-2c6cc96f.js +87 -0
- package/dist/esm/initialize-11d626ca.js +59 -0
- package/dist/esm/internal.js +10 -0
- package/dist/esm/master-fa8f2e43.js +318 -0
- package/dist/esm/master.js +10 -0
- package/dist/esm/multiplayer.js +14 -0
- package/dist/esm/plugin-random-087f861e.js +226 -0
- package/dist/esm/plugins.js +55 -0
- package/dist/esm/react-native.js +173 -0
- package/dist/esm/react.js +716 -0
- package/dist/esm/reducer-c46da7e5.js +1198 -0
- package/dist/esm/socketio-c22ffa65.js +455 -0
- package/dist/esm/testing.js +26 -0
- package/dist/esm/transport-ce07b771.js +35 -0
- package/dist/esm/turn-order-376d315e.js +1091 -0
- package/dist/esm/util-b6147cef.js +135 -0
- package/dist/types/packages/ai.d.ts +5 -0
- package/dist/types/packages/client.d.ts +3 -0
- package/dist/types/packages/core.d.ts +5 -0
- package/dist/types/packages/debug.d.ts +2 -0
- package/dist/types/packages/internal.d.ts +8 -0
- package/dist/types/packages/master.d.ts +2 -0
- package/dist/types/packages/multiplayer.d.ts +3 -0
- package/dist/types/packages/plugins.d.ts +3 -0
- package/dist/types/packages/react-native.d.ts +2 -0
- package/dist/types/packages/react.d.ts +3 -0
- package/dist/types/packages/server.d.ts +6 -0
- package/dist/types/packages/testing.d.ts +1 -0
- package/dist/types/src/ai/ai.d.ts +53 -0
- package/dist/types/src/ai/ai.test.d.ts +1 -0
- package/dist/types/src/ai/bot.d.ts +40 -0
- package/dist/types/src/ai/mcts-bot.d.ts +60 -0
- package/dist/types/src/ai/random-bot.d.ts +27 -0
- package/dist/types/src/client/client.d.ts +104 -0
- package/dist/types/src/client/client.test.d.ts +1 -0
- package/dist/types/src/client/debug/tests/debug.test.d.ts +1 -0
- package/dist/types/src/client/manager.d.ts +61 -0
- package/dist/types/src/client/react.d.ts +75 -0
- package/dist/types/src/client/react.ssr.test.d.ts +4 -0
- package/dist/types/src/client/react.test.d.ts +1 -0
- package/dist/types/src/client/transport/dummy.d.ts +18 -0
- package/dist/types/src/client/transport/local.d.ts +59 -0
- package/dist/types/src/client/transport/local.test.d.ts +1 -0
- package/dist/types/src/client/transport/socketio.d.ts +45 -0
- package/dist/types/src/client/transport/socketio.test.d.ts +1 -0
- package/dist/types/src/client/transport/transport.d.ts +50 -0
- package/dist/types/src/client/transport/transport.test.d.ts +1 -0
- package/dist/types/src/core/action-creators.d.ts +144 -0
- package/dist/types/src/core/action-types.d.ts +10 -0
- package/dist/types/src/core/backwards-compatibility.d.ts +12 -0
- package/dist/types/src/core/constants.d.ts +6 -0
- package/dist/types/src/core/errors.d.ts +15 -0
- package/dist/types/src/core/flow.d.ts +28 -0
- package/dist/types/src/core/flow.test.d.ts +1 -0
- package/dist/types/src/core/game-methods.d.ts +9 -0
- package/dist/types/src/core/game.d.ts +26 -0
- package/dist/types/src/core/game.test.d.ts +1 -0
- package/dist/types/src/core/initialize.d.ts +9 -0
- package/dist/types/src/core/logger.d.ts +2 -0
- package/dist/types/src/core/player-view.d.ts +7 -0
- package/dist/types/src/core/player-view.test.d.ts +1 -0
- package/dist/types/src/core/reducer.d.ts +155 -0
- package/dist/types/src/core/reducer.test.d.ts +1 -0
- package/dist/types/src/core/turn-order.d.ts +179 -0
- package/dist/types/src/core/turn-order.test.d.ts +8 -0
- package/dist/types/src/lobby/client.d.ts +194 -0
- package/dist/types/src/lobby/client.test.d.ts +1 -0
- package/dist/types/src/lobby/connection.d.ts +44 -0
- package/dist/types/src/lobby/connection.test.d.ts +1 -0
- package/dist/types/src/lobby/create-match-form.d.ts +26 -0
- package/dist/types/src/lobby/login-form.d.ts +23 -0
- package/dist/types/src/lobby/match-instance.d.ts +31 -0
- package/dist/types/src/lobby/react.d.ts +113 -0
- package/dist/types/src/lobby/react.ssr.test.d.ts +4 -0
- package/dist/types/src/lobby/react.test.d.ts +1 -0
- package/dist/types/src/master/filter-player-view.d.ts +96 -0
- package/dist/types/src/master/filter-player-view.test.d.ts +1 -0
- package/dist/types/src/master/master.d.ts +94 -0
- package/dist/types/src/master/master.test.d.ts +1 -0
- package/dist/types/src/plugins/events/events.d.ts +54 -0
- package/dist/types/src/plugins/events/events.test.d.ts +1 -0
- package/dist/types/src/plugins/main.d.ts +75 -0
- package/dist/types/src/plugins/main.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-events.d.ts +5 -0
- package/dist/types/src/plugins/plugin-immer.d.ts +7 -0
- package/dist/types/src/plugins/plugin-immer.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-log.d.ts +14 -0
- package/dist/types/src/plugins/plugin-log.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-player.d.ts +29 -0
- package/dist/types/src/plugins/plugin-player.test.d.ts +1 -0
- package/dist/types/src/plugins/plugin-random.d.ts +4 -0
- package/dist/types/src/plugins/plugin-serializable.d.ts +7 -0
- package/dist/types/src/plugins/plugin-serializable.test.d.ts +1 -0
- package/dist/types/src/plugins/random/random.alea.d.ts +19 -0
- package/dist/types/src/plugins/random/random.d.ts +54 -0
- package/dist/types/src/plugins/random/random.test.d.ts +1 -0
- package/dist/types/src/server/api.d.ts +13 -0
- package/dist/types/src/server/api.test.d.ts +1 -0
- package/dist/types/src/server/auth.d.ts +38 -0
- package/dist/types/src/server/auth.test.d.ts +1 -0
- package/dist/types/src/server/cors.d.ts +4 -0
- package/dist/types/src/server/cors.test.d.ts +1 -0
- package/dist/types/src/server/db/base.d.ts +192 -0
- package/dist/types/src/server/db/flatfile.d.ts +44 -0
- package/dist/types/src/server/db/flatfile.test.d.ts +1 -0
- package/dist/types/src/server/db/index.d.ts +4 -0
- package/dist/types/src/server/db/index.test.d.ts +1 -0
- package/dist/types/src/server/db/inmemory.d.ts +43 -0
- package/dist/types/src/server/db/inmemory.test.d.ts +1 -0
- package/dist/types/src/server/db/localstorage.d.ts +7 -0
- package/dist/types/src/server/db/localstorage.test.d.ts +1 -0
- package/dist/types/src/server/index.d.ts +68 -0
- package/dist/types/src/server/index.test.d.ts +1 -0
- package/dist/types/src/server/transport/pubsub/generic-pub-sub.d.ts +6 -0
- package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.d.ts +7 -0
- package/dist/types/src/server/transport/pubsub/in-memory-pub-sub.test.d.ts +1 -0
- package/dist/types/src/server/transport/socketio-simultaneous.test.d.ts +1 -0
- package/dist/types/src/server/transport/socketio.d.ts +65 -0
- package/dist/types/src/server/transport/socketio.test.d.ts +1 -0
- package/dist/types/src/server/util.d.ts +35 -0
- package/dist/types/src/testing/mock-random.d.ts +15 -0
- package/dist/types/src/testing/mock-random.test.d.ts +1 -0
- package/dist/types/src/types.d.ts +387 -0
- package/internal/package.json +7 -0
- package/master/package.json +7 -0
- package/multiplayer/package.json +7 -0
- package/package.json +211 -0
- package/plugins/package.json +7 -0
- package/react/package.json +7 -0
- package/react-native/package.json +7 -0
- package/server/package.json +6 -0
- package/src/ai/ai.test.ts +433 -0
- package/src/ai/ai.ts +84 -0
- package/src/ai/bot.ts +122 -0
- package/src/ai/mcts-bot.ts +331 -0
- package/src/ai/random-bot.ts +20 -0
- package/src/client/client.test.ts +993 -0
- package/src/client/client.ts +588 -0
- package/src/client/debug/Debug.svelte +239 -0
- package/src/client/debug/Menu.svelte +65 -0
- package/src/client/debug/ai/AI.svelte +215 -0
- package/src/client/debug/ai/Options.svelte +48 -0
- package/src/client/debug/info/Info.svelte +22 -0
- package/src/client/debug/info/Item.svelte +24 -0
- package/src/client/debug/log/Log.svelte +157 -0
- package/src/client/debug/log/LogEvent.svelte +149 -0
- package/src/client/debug/log/LogMetadata.svelte +7 -0
- package/src/client/debug/log/PhaseMarker.svelte +27 -0
- package/src/client/debug/log/TurnMarker.svelte +23 -0
- package/src/client/debug/main/ClientSwitcher.svelte +59 -0
- package/src/client/debug/main/Controls.svelte +58 -0
- package/src/client/debug/main/Hotkey.svelte +84 -0
- package/src/client/debug/main/InteractiveFunction.svelte +85 -0
- package/src/client/debug/main/Main.svelte +121 -0
- package/src/client/debug/main/Move.svelte +68 -0
- package/src/client/debug/main/PlayerInfo.svelte +70 -0
- package/src/client/debug/mcts/Action.svelte +22 -0
- package/src/client/debug/mcts/MCTS.svelte +78 -0
- package/src/client/debug/mcts/Table.svelte +98 -0
- package/src/client/debug/tests/JSONTree.mock.svelte +3 -0
- package/src/client/debug/tests/debug.test.ts +183 -0
- package/src/client/debug/utils/shortcuts.js +50 -0
- package/src/client/debug/utils/shortcuts.test.js +49 -0
- package/src/client/manager.ts +177 -0
- package/src/client/react-native.js +136 -0
- package/src/client/react-native.test.js +229 -0
- package/src/client/react.ssr.test.tsx +24 -0
- package/src/client/react.test.tsx +213 -0
- package/src/client/react.tsx +192 -0
- package/src/client/transport/dummy.ts +19 -0
- package/src/client/transport/local.test.ts +353 -0
- package/src/client/transport/local.ts +230 -0
- package/src/client/transport/socketio.test.ts +328 -0
- package/src/client/transport/socketio.ts +210 -0
- package/src/client/transport/transport.test.ts +27 -0
- package/src/client/transport/transport.ts +95 -0
- package/src/core/action-creators.ts +159 -0
- package/src/core/action-types.ts +18 -0
- package/src/core/backwards-compatibility.ts +23 -0
- package/src/core/constants.ts +6 -0
- package/src/core/errors.ts +35 -0
- package/src/core/flow.test.ts +2433 -0
- package/src/core/flow.ts +897 -0
- package/src/core/game-methods.ts +9 -0
- package/src/core/game.test.ts +286 -0
- package/src/core/game.ts +114 -0
- package/src/core/initialize.ts +77 -0
- package/src/core/logger.test.js +90 -0
- package/src/core/logger.ts +18 -0
- package/src/core/player-view.test.ts +50 -0
- package/src/core/player-view.ts +39 -0
- package/src/core/reducer.test.ts +991 -0
- package/src/core/reducer.ts +532 -0
- package/src/core/turn-order.test.ts +1123 -0
- package/src/core/turn-order.ts +473 -0
- package/src/lobby/client.test.ts +385 -0
- package/src/lobby/client.ts +358 -0
- package/src/lobby/connection.test.ts +207 -0
- package/src/lobby/connection.ts +162 -0
- package/src/lobby/create-match-form.tsx +122 -0
- package/src/lobby/login-form.tsx +75 -0
- package/src/lobby/match-instance.tsx +135 -0
- package/src/lobby/react.ssr.test.tsx +22 -0
- package/src/lobby/react.test.tsx +594 -0
- package/src/lobby/react.tsx +402 -0
- package/src/master/filter-player-view.test.ts +381 -0
- package/src/master/filter-player-view.ts +102 -0
- package/src/master/master.test.ts +1068 -0
- package/src/master/master.ts +492 -0
- package/src/plugins/events/events.test.ts +108 -0
- package/src/plugins/events/events.ts +209 -0
- package/src/plugins/main.test.ts +411 -0
- package/src/plugins/main.ts +314 -0
- package/src/plugins/plugin-events.ts +40 -0
- package/src/plugins/plugin-immer.test.ts +86 -0
- package/src/plugins/plugin-immer.ts +37 -0
- package/src/plugins/plugin-log.test.ts +37 -0
- package/src/plugins/plugin-log.ts +40 -0
- package/src/plugins/plugin-player.test.ts +172 -0
- package/src/plugins/plugin-player.ts +100 -0
- package/src/plugins/plugin-random.ts +40 -0
- package/src/plugins/plugin-serializable.test.ts +40 -0
- package/src/plugins/plugin-serializable.ts +55 -0
- package/src/plugins/random/random.alea.ts +109 -0
- package/src/plugins/random/random.test.ts +167 -0
- package/src/plugins/random/random.ts +198 -0
- package/src/server/api.test.ts +1699 -0
- package/src/server/api.ts +527 -0
- package/src/server/auth.test.ts +275 -0
- package/src/server/auth.ts +89 -0
- package/src/server/cors.test.ts +121 -0
- package/src/server/cors.ts +7 -0
- package/src/server/db/base.ts +296 -0
- package/src/server/db/flatfile.test.ts +221 -0
- package/src/server/db/flatfile.ts +228 -0
- package/src/server/db/index.test.ts +8 -0
- package/src/server/db/index.ts +12 -0
- package/src/server/db/inmemory.test.ts +143 -0
- package/src/server/db/inmemory.ts +143 -0
- package/src/server/db/localstorage.test.ts +73 -0
- package/src/server/db/localstorage.ts +44 -0
- package/src/server/index.test.ts +265 -0
- package/src/server/index.ts +175 -0
- package/src/server/transport/pubsub/generic-pub-sub.ts +11 -0
- package/src/server/transport/pubsub/in-memory-pub-sub.test.ts +47 -0
- package/src/server/transport/pubsub/in-memory-pub-sub.ts +28 -0
- package/src/server/transport/socketio-simultaneous.test.ts +603 -0
- package/src/server/transport/socketio.test.ts +303 -0
- package/src/server/transport/socketio.ts +279 -0
- package/src/server/util.ts +85 -0
- package/src/testing/mock-random.test.ts +45 -0
- package/src/testing/mock-random.ts +27 -0
- package/src/types.ts +511 -0
- package/testing/package.json +7 -0
|
@@ -0,0 +1,1699 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2018 The boardgame.io Authors
|
|
3
|
+
*
|
|
4
|
+
* Use of this source code is governed by a MIT-style
|
|
5
|
+
* license that can be found in the LICENSE file or at
|
|
6
|
+
* https://opensource.org/licenses/MIT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import request from 'supertest';
|
|
10
|
+
import Koa from 'koa';
|
|
11
|
+
import Router from '@koa/router';
|
|
12
|
+
import * as dateMock from 'jest-date-mock';
|
|
13
|
+
|
|
14
|
+
import { configureRouter, configureApp } from './api';
|
|
15
|
+
import { ProcessGameConfig } from '../core/game';
|
|
16
|
+
import { Auth } from './auth';
|
|
17
|
+
import * as StorageAPI from './db/base';
|
|
18
|
+
import { Origins } from './cors';
|
|
19
|
+
import type { Game, Server } from '../types';
|
|
20
|
+
|
|
21
|
+
jest.setTimeout(2000000000);
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
dateMock.clear();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
type StorageMocks = Record<
|
|
28
|
+
'createMatch' | 'setState' | 'fetch' | 'setMetadata' | 'listMatches' | 'wipe',
|
|
29
|
+
jest.Mock | ((...args: any[]) => any)
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
class AsyncStorage extends StorageAPI.Async {
|
|
33
|
+
public mocks: StorageMocks;
|
|
34
|
+
|
|
35
|
+
constructor(args: Partial<StorageMocks> = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this.mocks = {
|
|
38
|
+
createMatch: args.createMatch || jest.fn(),
|
|
39
|
+
setState: args.setState || jest.fn(),
|
|
40
|
+
fetch: args.fetch || jest.fn(() => ({})),
|
|
41
|
+
setMetadata: args.setMetadata || jest.fn(),
|
|
42
|
+
listMatches: args.listMatches || jest.fn(() => []),
|
|
43
|
+
wipe: args.wipe || jest.fn(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async connect() {}
|
|
48
|
+
|
|
49
|
+
async createMatch(...args) {
|
|
50
|
+
this.mocks.createMatch(...args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async fetch(...args) {
|
|
54
|
+
return this.mocks.fetch(...args);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async setState(...args) {
|
|
58
|
+
this.mocks.setState(...args);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async setMetadata(...args) {
|
|
62
|
+
this.mocks.setMetadata(...args);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async wipe(...args) {
|
|
66
|
+
this.mocks.wipe(...args);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async listMatches(...args) {
|
|
70
|
+
return this.mocks.listMatches(...args);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('.configureRouter', () => {
|
|
75
|
+
function addApiToServer({
|
|
76
|
+
app,
|
|
77
|
+
origins,
|
|
78
|
+
...args
|
|
79
|
+
}: {
|
|
80
|
+
app: Server.App;
|
|
81
|
+
origins?: Parameters<typeof configureApp>[2];
|
|
82
|
+
} & Omit<Parameters<typeof configureRouter>[0], 'router'>) {
|
|
83
|
+
const router = new Router<any, Server.AppCtx>();
|
|
84
|
+
configureRouter({ router, ...args });
|
|
85
|
+
configureApp(app, router, origins);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createApiServer(
|
|
89
|
+
args: Omit<Parameters<typeof addApiToServer>[0], 'app'>
|
|
90
|
+
) {
|
|
91
|
+
const app: Server.App = new Koa();
|
|
92
|
+
addApiToServer({ app, ...args });
|
|
93
|
+
return app;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('creating a game', () => {
|
|
97
|
+
let response;
|
|
98
|
+
let app: Koa;
|
|
99
|
+
let db: AsyncStorage;
|
|
100
|
+
const auth = new Auth();
|
|
101
|
+
let games: Game[];
|
|
102
|
+
const updatedAt = new Date(2020, 3, 4, 5, 6, 7);
|
|
103
|
+
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
db = new AsyncStorage();
|
|
106
|
+
games = [
|
|
107
|
+
{
|
|
108
|
+
name: 'foo',
|
|
109
|
+
setup: (_, setupData) =>
|
|
110
|
+
setupData
|
|
111
|
+
? {
|
|
112
|
+
colors: setupData.colors,
|
|
113
|
+
}
|
|
114
|
+
: {},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'validate',
|
|
118
|
+
setup: (_, setupData) =>
|
|
119
|
+
setupData
|
|
120
|
+
? {
|
|
121
|
+
numTokens: setupData.tokens,
|
|
122
|
+
}
|
|
123
|
+
: {},
|
|
124
|
+
validateSetupData: (setupData, numPlayers) =>
|
|
125
|
+
numPlayers == 2 && setupData.tokens !== 2
|
|
126
|
+
? 'Two player games must use two tokens'
|
|
127
|
+
: undefined,
|
|
128
|
+
minPlayers: 2,
|
|
129
|
+
maxPlayers: 2,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('for an unprotected lobby server', () => {
|
|
135
|
+
beforeEach(async () => {
|
|
136
|
+
dateMock.advanceTo(updatedAt);
|
|
137
|
+
|
|
138
|
+
delete process.env.API_SECRET;
|
|
139
|
+
|
|
140
|
+
const uuid = () => 'matchID';
|
|
141
|
+
app = createApiServer({ db, auth, games, uuid });
|
|
142
|
+
|
|
143
|
+
response = await request(app.callback())
|
|
144
|
+
.post('/games/foo/create')
|
|
145
|
+
.send({ numPlayers: 3 });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('is successful', () => {
|
|
149
|
+
expect(response.status).toEqual(200);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('creates game state and metadata', () => {
|
|
153
|
+
expect(db.mocks.createMatch).toHaveBeenCalledWith(
|
|
154
|
+
'matchID',
|
|
155
|
+
expect.objectContaining({
|
|
156
|
+
initialState: expect.objectContaining({
|
|
157
|
+
ctx: expect.objectContaining({
|
|
158
|
+
numPlayers: 3,
|
|
159
|
+
}),
|
|
160
|
+
}),
|
|
161
|
+
metadata: expect.objectContaining({
|
|
162
|
+
gameName: 'foo',
|
|
163
|
+
players: expect.objectContaining({
|
|
164
|
+
'0': expect.objectContaining({}),
|
|
165
|
+
'1': expect.objectContaining({}),
|
|
166
|
+
}),
|
|
167
|
+
unlisted: false,
|
|
168
|
+
createdAt: updatedAt.getTime(),
|
|
169
|
+
updatedAt: updatedAt.getTime(),
|
|
170
|
+
}),
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('returns match id', () => {
|
|
176
|
+
expect(response.body.matchID).not.toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('without numPlayers', () => {
|
|
180
|
+
beforeEach(async () => {
|
|
181
|
+
response = await request(app.callback()).post('/games/foo/create');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('uses default numPlayers', () => {
|
|
185
|
+
expect(db.mocks.createMatch).toHaveBeenCalledWith(
|
|
186
|
+
'matchID',
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
initialState: expect.objectContaining({
|
|
189
|
+
ctx: expect.objectContaining({
|
|
190
|
+
numPlayers: 2,
|
|
191
|
+
}),
|
|
192
|
+
}),
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('with invalid numPlayers', () => {
|
|
199
|
+
test('not enough players fails', async () => {
|
|
200
|
+
response = await request(app.callback())
|
|
201
|
+
.post('/games/validate/create')
|
|
202
|
+
.send({ numPlayers: 1 });
|
|
203
|
+
|
|
204
|
+
expect(response.status).toEqual(400);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('too many players fails', async () => {
|
|
208
|
+
response = await request(app.callback())
|
|
209
|
+
.post('/games/validate/create')
|
|
210
|
+
.send({ numPlayers: 3 });
|
|
211
|
+
|
|
212
|
+
expect(response.status).toEqual(400);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('invalid type fails', async () => {
|
|
216
|
+
response = await request(app.callback())
|
|
217
|
+
.post('/games/validate/create')
|
|
218
|
+
.send({ numPlayers: 'hello' });
|
|
219
|
+
|
|
220
|
+
expect(response.status).toEqual(400);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('for an unknown game name', () => {
|
|
225
|
+
beforeEach(async () => {
|
|
226
|
+
response = await request(app.callback()).post('/games/bar/create');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('returns 404 error', () => {
|
|
230
|
+
expect(response.status).toEqual(404);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('with setupData', () => {
|
|
235
|
+
beforeEach(async () => {
|
|
236
|
+
response = await request(app.callback())
|
|
237
|
+
.post('/games/foo/create')
|
|
238
|
+
.send({
|
|
239
|
+
setupData: {
|
|
240
|
+
colors: {
|
|
241
|
+
'0': 'green',
|
|
242
|
+
'1': 'red',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('includes setupData in metadata', () => {
|
|
249
|
+
expect(db.mocks.createMatch).toHaveBeenCalledWith(
|
|
250
|
+
'matchID',
|
|
251
|
+
expect.objectContaining({
|
|
252
|
+
metadata: expect.objectContaining({
|
|
253
|
+
setupData: expect.objectContaining({
|
|
254
|
+
colors: expect.objectContaining({
|
|
255
|
+
'0': 'green',
|
|
256
|
+
'1': 'red',
|
|
257
|
+
}),
|
|
258
|
+
}),
|
|
259
|
+
}),
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('passes setupData to game setup function', () => {
|
|
265
|
+
expect(db.mocks.createMatch).toHaveBeenCalledWith(
|
|
266
|
+
'matchID',
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
initialState: expect.objectContaining({
|
|
269
|
+
G: expect.objectContaining({
|
|
270
|
+
colors: {
|
|
271
|
+
'0': 'green',
|
|
272
|
+
'1': 'red',
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
}),
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('with setupData validation', () => {
|
|
282
|
+
test('creates game if validation passes', async () => {
|
|
283
|
+
response = await request(app.callback())
|
|
284
|
+
.post('/games/validate/create')
|
|
285
|
+
.send({
|
|
286
|
+
numPlayers: 2,
|
|
287
|
+
setupData: {
|
|
288
|
+
tokens: 2,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(response.status).toEqual(200);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('returns error if validation fails', async () => {
|
|
296
|
+
response = await request(app.callback())
|
|
297
|
+
.post('/games/validate/create')
|
|
298
|
+
.send({
|
|
299
|
+
numPlayers: 2,
|
|
300
|
+
setupData: {
|
|
301
|
+
tokens: 3,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(response.status).toEqual(400);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('with unlisted option', () => {
|
|
310
|
+
beforeEach(async () => {
|
|
311
|
+
response = await request(app.callback())
|
|
312
|
+
.post('/games/foo/create')
|
|
313
|
+
.send({ unlisted: true });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('sets unlisted in metadata', () => {
|
|
317
|
+
expect(db.mocks.createMatch).toHaveBeenCalledWith(
|
|
318
|
+
'matchID',
|
|
319
|
+
expect.objectContaining({
|
|
320
|
+
metadata: expect.objectContaining({
|
|
321
|
+
unlisted: true,
|
|
322
|
+
}),
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('for a protected lobby', () => {
|
|
330
|
+
beforeEach(() => {
|
|
331
|
+
process.env.API_SECRET = 'protected';
|
|
332
|
+
app = createApiServer({ db, auth, games });
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('without the lobby token', () => {
|
|
336
|
+
beforeEach(async () => {
|
|
337
|
+
response = await request(app.callback()).post('/games/foo/create');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('fails', () => {
|
|
341
|
+
expect(response.status).toEqual(403);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('with the lobby token', () => {
|
|
346
|
+
beforeEach(async () => {
|
|
347
|
+
response = await request(app.callback())
|
|
348
|
+
.post('/games/foo/create')
|
|
349
|
+
.set('API-Secret', 'protected');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('succeeds', () => {
|
|
353
|
+
expect(response.status).toEqual(200);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('joining a room', () => {
|
|
360
|
+
let response;
|
|
361
|
+
let db: AsyncStorage;
|
|
362
|
+
const auth = new Auth();
|
|
363
|
+
let games: Game[];
|
|
364
|
+
let credentials: string;
|
|
365
|
+
|
|
366
|
+
beforeEach(() => {
|
|
367
|
+
credentials = 'SECRET';
|
|
368
|
+
games = [ProcessGameConfig({ name: 'foo' })];
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('for an unprotected lobby', () => {
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
delete process.env.API_SECRET;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('when the game does not exist', () => {
|
|
377
|
+
beforeEach(async () => {
|
|
378
|
+
db = new AsyncStorage({
|
|
379
|
+
fetch: () => ({ metadata: null }),
|
|
380
|
+
});
|
|
381
|
+
const app = createApiServer({ db, auth, games });
|
|
382
|
+
|
|
383
|
+
response = await request(app.callback())
|
|
384
|
+
.post('/games/foo/1/join')
|
|
385
|
+
.send('playerID=0&playerName=alice');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('throws a "not found" error', async () => {
|
|
389
|
+
expect(response.status).toEqual(404);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('when the game does exist', () => {
|
|
394
|
+
beforeEach(async () => {
|
|
395
|
+
db = new AsyncStorage({
|
|
396
|
+
fetch: async () => {
|
|
397
|
+
return {
|
|
398
|
+
metadata: {
|
|
399
|
+
players: {
|
|
400
|
+
'0': {},
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('when the playerID is available', () => {
|
|
409
|
+
beforeEach(async () => {
|
|
410
|
+
const app = createApiServer({
|
|
411
|
+
db,
|
|
412
|
+
auth: new Auth({ generateCredentials: () => credentials }),
|
|
413
|
+
games,
|
|
414
|
+
uuid: () => 'matchID',
|
|
415
|
+
});
|
|
416
|
+
response = await request(app.callback())
|
|
417
|
+
.post('/games/foo/1/join')
|
|
418
|
+
.send({ playerID: 0, playerName: 'alice' });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('is successful', async () => {
|
|
422
|
+
expect(response.status).toEqual(200);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('returns the player credentials', async () => {
|
|
426
|
+
expect(response.body.playerCredentials).toEqual(credentials);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('updates the player name', async () => {
|
|
430
|
+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
|
|
431
|
+
'1',
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
players: expect.objectContaining({
|
|
434
|
+
'0': expect.objectContaining({
|
|
435
|
+
name: 'alice',
|
|
436
|
+
}),
|
|
437
|
+
}),
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe('when custom data is provided', () => {
|
|
443
|
+
beforeEach(async () => {
|
|
444
|
+
const app = createApiServer({ db, auth, games });
|
|
445
|
+
response = await request(app.callback())
|
|
446
|
+
.post('/games/foo/1/join')
|
|
447
|
+
.send({ playerID: 0, playerName: 'alice', data: 99 });
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('updates the player data', async () => {
|
|
451
|
+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
|
|
452
|
+
'1',
|
|
453
|
+
expect.objectContaining({
|
|
454
|
+
players: expect.objectContaining({
|
|
455
|
+
'0': expect.objectContaining({
|
|
456
|
+
data: 99,
|
|
457
|
+
}),
|
|
458
|
+
}),
|
|
459
|
+
})
|
|
460
|
+
);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('when the playerID does not exist', () => {
|
|
466
|
+
beforeEach(async () => {
|
|
467
|
+
const app = createApiServer({ db, auth, games });
|
|
468
|
+
response = await request(app.callback())
|
|
469
|
+
.post('/games/foo/1/join')
|
|
470
|
+
.send('playerID=1&playerName=alice');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('throws error 404', async () => {
|
|
474
|
+
expect(response.status).toEqual(404);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('when playerID is omitted', () => {
|
|
479
|
+
beforeEach(async () => {
|
|
480
|
+
const app = createApiServer({
|
|
481
|
+
db,
|
|
482
|
+
auth: new Auth({ generateCredentials: () => credentials }),
|
|
483
|
+
games,
|
|
484
|
+
uuid: () => 'matchID',
|
|
485
|
+
});
|
|
486
|
+
response = await request(app.callback())
|
|
487
|
+
.post('/games/foo/1/join')
|
|
488
|
+
.send('playerName=alice');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe('numPlayers is reached in match', () => {
|
|
492
|
+
beforeEach(async () => {
|
|
493
|
+
db = new AsyncStorage({
|
|
494
|
+
fetch: async () => {
|
|
495
|
+
return {
|
|
496
|
+
metadata: {
|
|
497
|
+
players: {
|
|
498
|
+
'0': { name: 'alice' },
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
const app = createApiServer({ db, auth, games });
|
|
505
|
+
response = await request(app.callback())
|
|
506
|
+
.post('/games/foo/1/join')
|
|
507
|
+
.send('playerName=bob');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test('throws error 409', async () => {
|
|
511
|
+
expect(response.status).toEqual(409);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('is successful', async () => {
|
|
516
|
+
expect(response.status).toEqual(200);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('returns the player credentials', async () => {
|
|
520
|
+
expect(response.body.playerCredentials).toEqual(credentials);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('returns the playerID', async () => {
|
|
524
|
+
expect(response.body.playerID).toEqual('0');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('updates the player name', async () => {
|
|
528
|
+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
|
|
529
|
+
'1',
|
|
530
|
+
expect.objectContaining({
|
|
531
|
+
players: expect.objectContaining({
|
|
532
|
+
'0': expect.objectContaining({
|
|
533
|
+
name: 'alice',
|
|
534
|
+
}),
|
|
535
|
+
}),
|
|
536
|
+
})
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('when playerName is omitted', () => {
|
|
542
|
+
beforeEach(async () => {
|
|
543
|
+
const app = createApiServer({ db, auth, games });
|
|
544
|
+
response = await request(app.callback())
|
|
545
|
+
.post('/games/foo/1/join')
|
|
546
|
+
.send('playerID=1');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('throws error 403', async () => {
|
|
550
|
+
expect(response.status).toEqual(403);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe('when the playerID is not available', () => {
|
|
555
|
+
beforeEach(async () => {
|
|
556
|
+
db = new AsyncStorage({
|
|
557
|
+
fetch: async () => {
|
|
558
|
+
return {
|
|
559
|
+
metadata: {
|
|
560
|
+
players: {
|
|
561
|
+
'0': {
|
|
562
|
+
credentials,
|
|
563
|
+
name: 'bob',
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const app = createApiServer({ db, auth, games });
|
|
572
|
+
|
|
573
|
+
response = await request(app.callback())
|
|
574
|
+
.post('/games/foo/1/join')
|
|
575
|
+
.send('playerID=0&playerName=alice');
|
|
576
|
+
});
|
|
577
|
+
test('throws error 409', async () => {
|
|
578
|
+
expect(response.status).toEqual(409);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
describe('rename with deprecated endpoint', () => {
|
|
586
|
+
let response;
|
|
587
|
+
let db: AsyncStorage;
|
|
588
|
+
const auth = new Auth();
|
|
589
|
+
let games: Game[];
|
|
590
|
+
const warnMsg =
|
|
591
|
+
'This endpoint /rename is deprecated. Please use /update instead.';
|
|
592
|
+
|
|
593
|
+
beforeEach(() => {
|
|
594
|
+
games = [ProcessGameConfig({ name: 'foo' })];
|
|
595
|
+
console.warn = jest.fn();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe('for an unprotected lobby', () => {
|
|
599
|
+
beforeEach(() => {
|
|
600
|
+
delete process.env.API_SECRET;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe('when the game does not exist', () => {
|
|
604
|
+
test('throws a "not found" error', async () => {
|
|
605
|
+
db = new AsyncStorage({
|
|
606
|
+
fetch: async () => ({ metadata: null }),
|
|
607
|
+
});
|
|
608
|
+
const app = createApiServer({ db, auth, games });
|
|
609
|
+
response = await request(app.callback())
|
|
610
|
+
.post('/games/foo/1/rename')
|
|
611
|
+
.send('playerID=0&playerName=alice&newName=ali');
|
|
612
|
+
expect(response.status).toEqual(404);
|
|
613
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe('when the game does exist', () => {
|
|
618
|
+
describe('when the playerID does exist', () => {
|
|
619
|
+
beforeEach(async () => {
|
|
620
|
+
db = new AsyncStorage({
|
|
621
|
+
fetch: async () => {
|
|
622
|
+
return {
|
|
623
|
+
metadata: {
|
|
624
|
+
players: {
|
|
625
|
+
'0': {
|
|
626
|
+
name: 'alice',
|
|
627
|
+
credentials: 'SECRET1',
|
|
628
|
+
},
|
|
629
|
+
'1': {
|
|
630
|
+
name: 'bob',
|
|
631
|
+
credentials: 'SECRET2',
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
const app = createApiServer({ db, auth, games });
|
|
639
|
+
response = await request(app.callback())
|
|
640
|
+
.post('/games/foo/1/rename')
|
|
641
|
+
.send('playerID=0&credentials=SECRET1&newName=ali');
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
describe('when the playerName is not a string', () => {
|
|
645
|
+
test('throws newName must be a string', async () => {
|
|
646
|
+
const app = createApiServer({ db, auth, games });
|
|
647
|
+
response = await request(app.callback())
|
|
648
|
+
.post('/games/foo/1/rename')
|
|
649
|
+
.send({ playerID: 0, credentials: 'SECRET1', newName: 2 });
|
|
650
|
+
expect(response.text).toEqual(
|
|
651
|
+
'newName must be a string, got number'
|
|
652
|
+
);
|
|
653
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test('is successful', async () => {
|
|
658
|
+
expect(response.status).toEqual(200);
|
|
659
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test('updates the players', async () => {
|
|
663
|
+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
|
|
664
|
+
'1',
|
|
665
|
+
expect.objectContaining({
|
|
666
|
+
players: expect.objectContaining({
|
|
667
|
+
'0': expect.objectContaining({
|
|
668
|
+
name: 'ali',
|
|
669
|
+
}),
|
|
670
|
+
}),
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describe('when the playerID does not exist', () => {
|
|
678
|
+
test('throws error 404', async () => {
|
|
679
|
+
const app = createApiServer({ db, auth, games });
|
|
680
|
+
response = await request(app.callback())
|
|
681
|
+
.post('/games/foo/1/rename')
|
|
682
|
+
.send('playerID=2&credentials=SECRET1&newName=joe');
|
|
683
|
+
expect(response.status).toEqual(404);
|
|
684
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
describe('when the credentials are invalid', () => {
|
|
689
|
+
test('throws error 404', async () => {
|
|
690
|
+
const app = createApiServer({ db, auth, games });
|
|
691
|
+
response = await request(app.callback())
|
|
692
|
+
.post('/games/foo/1/rename')
|
|
693
|
+
.send('playerID=0&credentials=SECRET2&newName=mike');
|
|
694
|
+
expect(response.status).toEqual(403);
|
|
695
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
describe('when playerID is omitted', () => {
|
|
700
|
+
beforeEach(async () => {
|
|
701
|
+
const app = createApiServer({ db, auth, games });
|
|
702
|
+
response = await request(app.callback())
|
|
703
|
+
.post('/games/foo/1/rename')
|
|
704
|
+
.send('credentials=foo&newName=bill');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('throws error 403', async () => {
|
|
708
|
+
expect(response.status).toEqual(403);
|
|
709
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe('when newName is omitted', () => {
|
|
714
|
+
beforeEach(async () => {
|
|
715
|
+
const app = createApiServer({ db, auth, games });
|
|
716
|
+
response = await request(app.callback())
|
|
717
|
+
.post('/games/foo/1/rename')
|
|
718
|
+
.send('credentials=foo&playerID=0');
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('throws error 403', async () => {
|
|
722
|
+
expect(response.status).toEqual(403);
|
|
723
|
+
expect(console.warn).toBeCalledWith(warnMsg);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
describe('rename with update endpoint', () => {
|
|
731
|
+
let response;
|
|
732
|
+
let db: AsyncStorage;
|
|
733
|
+
const auth = new Auth();
|
|
734
|
+
let games: Game[];
|
|
735
|
+
|
|
736
|
+
beforeEach(() => {
|
|
737
|
+
games = [ProcessGameConfig({ name: 'foo' })];
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
describe('for an unprotected lobby', () => {
|
|
741
|
+
beforeEach(() => {
|
|
742
|
+
delete process.env.API_SECRET;
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe('when the game does not exist', () => {
|
|
746
|
+
test('throws game not found', async () => {
|
|
747
|
+
db = new AsyncStorage({
|
|
748
|
+
fetch: async () => ({ metadata: null }),
|
|
749
|
+
});
|
|
750
|
+
const app = createApiServer({ db, auth, games });
|
|
751
|
+
response = await request(app.callback())
|
|
752
|
+
.post('/games/foo/1/update')
|
|
753
|
+
.send('playerID=0&playerName=alice&newName=ali');
|
|
754
|
+
expect(response.text).toEqual('Match 1 not found');
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
describe('when the game does exist', () => {
|
|
759
|
+
describe('when the playerID does exist', () => {
|
|
760
|
+
beforeEach(async () => {
|
|
761
|
+
db = new AsyncStorage({
|
|
762
|
+
fetch: async () => {
|
|
763
|
+
return {
|
|
764
|
+
metadata: {
|
|
765
|
+
players: {
|
|
766
|
+
'0': {
|
|
767
|
+
name: 'alice',
|
|
768
|
+
credentials: 'SECRET1',
|
|
769
|
+
},
|
|
770
|
+
'1': {
|
|
771
|
+
name: 'bob',
|
|
772
|
+
credentials: 'SECRET2',
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
const app = createApiServer({ db, auth, games });
|
|
780
|
+
response = await request(app.callback())
|
|
781
|
+
.post('/games/foo/1/update')
|
|
782
|
+
.send('playerID=0&credentials=SECRET1&newName=ali');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
describe('when the playerName is not a string', () => {
|
|
786
|
+
test('throws newName must be a string', async () => {
|
|
787
|
+
const app = createApiServer({ db, auth, games });
|
|
788
|
+
response = await request(app.callback())
|
|
789
|
+
.post('/games/foo/1/update')
|
|
790
|
+
.send({ playerID: 0, credentials: 'SECRET1', newName: 2 });
|
|
791
|
+
expect(response.text).toEqual(
|
|
792
|
+
'newName must be a string, got number'
|
|
793
|
+
);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test('is successful', async () => {
|
|
798
|
+
expect(response.status).toEqual(200);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test('updates the players', async () => {
|
|
802
|
+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
|
|
803
|
+
'1',
|
|
804
|
+
expect.objectContaining({
|
|
805
|
+
players: expect.objectContaining({
|
|
806
|
+
'0': expect.objectContaining({
|
|
807
|
+
name: 'ali',
|
|
808
|
+
}),
|
|
809
|
+
}),
|
|
810
|
+
})
|
|
811
|
+
);
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
describe('when the playerID does not exist', () => {
|
|
816
|
+
test('throws player not found', async () => {
|
|
817
|
+
const app = createApiServer({ db, auth, games });
|
|
818
|
+
response = await request(app.callback())
|
|
819
|
+
.post('/games/foo/1/update')
|
|
820
|
+
.send('playerID=2&credentials=SECRET1&newName=joe');
|
|
821
|
+
expect(response.text).toEqual('Player 2 not found');
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
describe('when the credentials are invalid', () => {
|
|
826
|
+
test('throws invalid credentials', async () => {
|
|
827
|
+
const app = createApiServer({ db, auth, games });
|
|
828
|
+
response = await request(app.callback())
|
|
829
|
+
.post('/games/foo/1/update')
|
|
830
|
+
.send('playerID=0&credentials=SECRET2&newName=mike');
|
|
831
|
+
expect(response.text).toEqual('Invalid credentials SECRET2');
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
describe('when playerID is omitted', () => {
|
|
836
|
+
beforeEach(async () => {
|
|
837
|
+
const app = createApiServer({ db, auth, games });
|
|
838
|
+
response = await request(app.callback())
|
|
839
|
+
.post('/games/foo/1/update')
|
|
840
|
+
.send('credentials=foo&newName=bill');
|
|
841
|
+
});
|
|
842
|
+
test('throws playerID is required', async () => {
|
|
843
|
+
expect(response.text).toEqual('playerID is required');
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
describe('when newName is omitted', () => {
|
|
848
|
+
beforeEach(async () => {
|
|
849
|
+
const app = createApiServer({ db, auth, games });
|
|
850
|
+
response = await request(app.callback())
|
|
851
|
+
.post('/games/foo/1/update')
|
|
852
|
+
.send('credentials=foo&playerID=0');
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test('throws newName is required', async () => {
|
|
856
|
+
expect(response.text).toEqual('newName or data is required');
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
describe('updating player metadata', () => {
|
|
864
|
+
let response;
|
|
865
|
+
let db: AsyncStorage;
|
|
866
|
+
const auth = new Auth();
|
|
867
|
+
let games: Game[];
|
|
868
|
+
|
|
869
|
+
beforeEach(() => {
|
|
870
|
+
games = [ProcessGameConfig({ name: 'foo' })];
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
describe('for an unprotected lobby', () => {
|
|
874
|
+
beforeEach(() => {
|
|
875
|
+
delete process.env.API_SECRET;
|
|
876
|
+
});
|
|
877
|
+
describe('when the game does not exist', () => {
|
|
878
|
+
test('throws game not found', async () => {
|
|
879
|
+
db = new AsyncStorage({
|
|
880
|
+
fetch: async () => ({ metadata: null }),
|
|
881
|
+
});
|
|
882
|
+
const app = createApiServer({ db, auth, games });
|
|
883
|
+
response = await request(app.callback())
|
|
884
|
+
.post('/games/foo/1/update')
|
|
885
|
+
.send({ playerID: 0, data: { subdata: 'text' } });
|
|
886
|
+
expect(response.text).toEqual('Match 1 not found');
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
describe('when the game does exist', () => {
|
|
891
|
+
describe('when the playerID does exist', () => {
|
|
892
|
+
beforeEach(async () => {
|
|
893
|
+
db = new AsyncStorage({
|
|
894
|
+
fetch: async () => {
|
|
895
|
+
return {
|
|
896
|
+
metadata: {
|
|
897
|
+
players: {
|
|
898
|
+
'0': {
|
|
899
|
+
name: 'alice',
|
|
900
|
+
credentials: 'SECRET1',
|
|
901
|
+
},
|
|
902
|
+
'1': {
|
|
903
|
+
name: 'bob',
|
|
904
|
+
credentials: 'SECRET2',
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
const app = createApiServer({ db, auth, games });
|
|
912
|
+
response = await request(app.callback())
|
|
913
|
+
.post('/games/foo/1/update')
|
|
914
|
+
.send({
|
|
915
|
+
playerID: 0,
|
|
916
|
+
credentials: 'SECRET1',
|
|
917
|
+
data: { subdata: 'text' },
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('is successful', async () => {
|
|
922
|
+
expect(response.status).toEqual(200);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
test('updates the players', async () => {
|
|
926
|
+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
|
|
927
|
+
'1',
|
|
928
|
+
expect.objectContaining({
|
|
929
|
+
players: expect.objectContaining({
|
|
930
|
+
'0': expect.objectContaining({
|
|
931
|
+
data: expect.objectContaining({
|
|
932
|
+
subdata: 'text',
|
|
933
|
+
}),
|
|
934
|
+
}),
|
|
935
|
+
}),
|
|
936
|
+
})
|
|
937
|
+
);
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
describe('when the playerID does not exist', () => {
|
|
942
|
+
test('throws playerID not found', async () => {
|
|
943
|
+
const app = createApiServer({ db, auth, games });
|
|
944
|
+
response = await request(app.callback())
|
|
945
|
+
.post('/games/foo/1/update')
|
|
946
|
+
.send({
|
|
947
|
+
playerID: 2,
|
|
948
|
+
credentials: 'SECRET1',
|
|
949
|
+
data: { subdata: 'text' },
|
|
950
|
+
});
|
|
951
|
+
expect(response.text).toEqual('Player 2 not found');
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
describe('when the credentials are invalid', () => {
|
|
956
|
+
test('invalid credentials', async () => {
|
|
957
|
+
const app = createApiServer({ db, auth, games });
|
|
958
|
+
response = await request(app.callback())
|
|
959
|
+
.post('/games/foo/1/update')
|
|
960
|
+
.send({
|
|
961
|
+
playerID: 0,
|
|
962
|
+
credentials: 'SECRET2',
|
|
963
|
+
data: { subdata: 'text' },
|
|
964
|
+
});
|
|
965
|
+
expect(response.text).toEqual('Invalid credentials SECRET2');
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
describe('when playerID is omitted', () => {
|
|
970
|
+
beforeEach(async () => {
|
|
971
|
+
const app = createApiServer({ db, auth, games });
|
|
972
|
+
response = await request(app.callback())
|
|
973
|
+
.post('/games/foo/1/update')
|
|
974
|
+
.send({ credentials: 'foo', data: { subdata: 'text' } });
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test('throws playerID is required', async () => {
|
|
978
|
+
expect(response.text).toEqual('playerID is required');
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
describe('when data is omitted', () => {
|
|
983
|
+
beforeEach(async () => {
|
|
984
|
+
const app = createApiServer({ db, auth, games });
|
|
985
|
+
response = await request(app.callback())
|
|
986
|
+
.post('/games/foo/1/update')
|
|
987
|
+
.send({ playerID: 0, credentials: 'foo' });
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
test('throws data is required', async () => {
|
|
991
|
+
expect(response.text).toEqual('newName or data is required');
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
describe('leaving a room', () => {
|
|
999
|
+
let response;
|
|
1000
|
+
let db: AsyncStorage;
|
|
1001
|
+
const auth = new Auth();
|
|
1002
|
+
let games: Game[];
|
|
1003
|
+
|
|
1004
|
+
beforeEach(() => {
|
|
1005
|
+
games = [ProcessGameConfig({ name: 'foo' })];
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
describe('for an unprotected lobby', () => {
|
|
1009
|
+
beforeEach(() => {
|
|
1010
|
+
delete process.env.API_SECRET;
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
describe('when the game does not exist', () => {
|
|
1014
|
+
test('throws a "not found" error', async () => {
|
|
1015
|
+
db = new AsyncStorage({
|
|
1016
|
+
fetch: async () => ({ metadata: null }),
|
|
1017
|
+
});
|
|
1018
|
+
const app = createApiServer({ db, auth, games });
|
|
1019
|
+
response = await request(app.callback())
|
|
1020
|
+
.post('/games/foo/1/leave')
|
|
1021
|
+
.send('playerID=0&playerName=alice');
|
|
1022
|
+
expect(response.status).toEqual(404);
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
describe('when the game does exist', () => {
|
|
1027
|
+
describe('when the playerID does exist', () => {
|
|
1028
|
+
beforeEach(async () => {
|
|
1029
|
+
db = new AsyncStorage({
|
|
1030
|
+
fetch: async () => {
|
|
1031
|
+
return {
|
|
1032
|
+
metadata: {
|
|
1033
|
+
players: {
|
|
1034
|
+
'0': {
|
|
1035
|
+
name: 'alice',
|
|
1036
|
+
credentials: 'SECRET1',
|
|
1037
|
+
},
|
|
1038
|
+
'1': {
|
|
1039
|
+
name: 'bob',
|
|
1040
|
+
credentials: 'SECRET2',
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
};
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
const app = createApiServer({ db, auth, games });
|
|
1048
|
+
response = await request(app.callback())
|
|
1049
|
+
.post('/games/foo/1/leave')
|
|
1050
|
+
.send('playerID=0&credentials=SECRET1');
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test('is successful', async () => {
|
|
1054
|
+
expect(response.status).toEqual(200);
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
test('updates the players', async () => {
|
|
1058
|
+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
|
|
1059
|
+
'1',
|
|
1060
|
+
expect.objectContaining({
|
|
1061
|
+
players: expect.objectContaining({
|
|
1062
|
+
'0': expect.objectContaining({}),
|
|
1063
|
+
'1': expect.objectContaining({
|
|
1064
|
+
name: 'bob',
|
|
1065
|
+
credentials: 'SECRET2',
|
|
1066
|
+
}),
|
|
1067
|
+
}),
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
describe('when there are not players left', () => {
|
|
1073
|
+
test('removes the game', async () => {
|
|
1074
|
+
db = new AsyncStorage({
|
|
1075
|
+
fetch: async () => {
|
|
1076
|
+
return {
|
|
1077
|
+
metadata: {
|
|
1078
|
+
players: {
|
|
1079
|
+
'0': {
|
|
1080
|
+
name: 'alice',
|
|
1081
|
+
credentials: 'SECRET1',
|
|
1082
|
+
},
|
|
1083
|
+
'1': {
|
|
1084
|
+
credentials: 'SECRET2',
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
};
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
const app = createApiServer({ db, auth, games });
|
|
1092
|
+
response = await request(app.callback())
|
|
1093
|
+
.post('/games/foo/1/leave')
|
|
1094
|
+
.send('playerID=0&credentials=SECRET1');
|
|
1095
|
+
expect(db.mocks.wipe).toHaveBeenCalledWith('1');
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
describe('when the playerID does not exist', () => {
|
|
1101
|
+
test('throws error 404', async () => {
|
|
1102
|
+
const app = createApiServer({ db, auth, games });
|
|
1103
|
+
response = await request(app.callback())
|
|
1104
|
+
.post('/games/foo/1/leave')
|
|
1105
|
+
.send('playerID=2&credentials=SECRET1');
|
|
1106
|
+
expect(response.status).toEqual(404);
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
describe('when the credentials are invalid', () => {
|
|
1111
|
+
test('throws error 404', async () => {
|
|
1112
|
+
const app = createApiServer({ db, auth, games });
|
|
1113
|
+
response = await request(app.callback())
|
|
1114
|
+
.post('/games/foo/1/leave')
|
|
1115
|
+
.send('playerID=0&credentials=SECRET2');
|
|
1116
|
+
expect(response.status).toEqual(403);
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
describe('when playerID is omitted', () => {
|
|
1120
|
+
beforeEach(async () => {
|
|
1121
|
+
const app = createApiServer({ db, auth, games });
|
|
1122
|
+
response = await request(app.callback())
|
|
1123
|
+
.post('/games/foo/1/leave')
|
|
1124
|
+
.send('credentials=foo');
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
test('throws error 403', async () => {
|
|
1128
|
+
expect(response.status).toEqual(403);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
describe('requesting game list', () => {
|
|
1136
|
+
let db: AsyncStorage;
|
|
1137
|
+
const auth = new Auth();
|
|
1138
|
+
beforeEach(() => {
|
|
1139
|
+
delete process.env.API_SECRET;
|
|
1140
|
+
db = new AsyncStorage();
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
describe('when given 2 games', () => {
|
|
1144
|
+
let response;
|
|
1145
|
+
beforeEach(async () => {
|
|
1146
|
+
const games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
|
|
1147
|
+
const app = createApiServer({ db, auth, games });
|
|
1148
|
+
|
|
1149
|
+
response = await request(app.callback()).get('/games');
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test('should get 2 games', async () => {
|
|
1153
|
+
expect(JSON.parse(response.text)).toEqual(['foo', 'bar']);
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
describe('play again', () => {
|
|
1159
|
+
let response;
|
|
1160
|
+
let db: AsyncStorage;
|
|
1161
|
+
const auth = new Auth();
|
|
1162
|
+
let games: Game[];
|
|
1163
|
+
|
|
1164
|
+
beforeEach(() => {
|
|
1165
|
+
games = [ProcessGameConfig({ name: 'foo' })];
|
|
1166
|
+
delete process.env.API_SECRET;
|
|
1167
|
+
db = new AsyncStorage({
|
|
1168
|
+
fetch: async () => {
|
|
1169
|
+
return {
|
|
1170
|
+
metadata: {
|
|
1171
|
+
setupData: {
|
|
1172
|
+
colors: {
|
|
1173
|
+
'0': 'green',
|
|
1174
|
+
'1': 'red',
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
players: {
|
|
1178
|
+
'0': {
|
|
1179
|
+
name: 'alice',
|
|
1180
|
+
credentials: 'SECRET1',
|
|
1181
|
+
},
|
|
1182
|
+
'1': {
|
|
1183
|
+
name: 'bob',
|
|
1184
|
+
credentials: 'SECRET2',
|
|
1185
|
+
},
|
|
1186
|
+
'2': {
|
|
1187
|
+
name: 'chris',
|
|
1188
|
+
credentials: 'SECRET3',
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
};
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
test('creates new game data', async () => {
|
|
1198
|
+
const uuid = () => 'newGameID';
|
|
1199
|
+
const app = createApiServer({ db, auth, games, uuid });
|
|
1200
|
+
|
|
1201
|
+
response = await request(app.callback())
|
|
1202
|
+
.post('/games/foo/1/playAgain')
|
|
1203
|
+
.send({
|
|
1204
|
+
playerID: 0,
|
|
1205
|
+
credentials: 'SECRET1',
|
|
1206
|
+
numPlayers: 4,
|
|
1207
|
+
setupData: {
|
|
1208
|
+
colors: {
|
|
1209
|
+
'3': 'blue',
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
expect(db.mocks.createMatch).toHaveBeenCalledWith(
|
|
1214
|
+
'newGameID',
|
|
1215
|
+
expect.objectContaining({
|
|
1216
|
+
initialState: expect.objectContaining({
|
|
1217
|
+
ctx: expect.objectContaining({
|
|
1218
|
+
numPlayers: 4,
|
|
1219
|
+
}),
|
|
1220
|
+
}),
|
|
1221
|
+
metadata: expect.objectContaining({
|
|
1222
|
+
setupData: expect.objectContaining({
|
|
1223
|
+
colors: expect.objectContaining({
|
|
1224
|
+
'3': 'blue',
|
|
1225
|
+
}),
|
|
1226
|
+
}),
|
|
1227
|
+
}),
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1230
|
+
expect(response.body.nextMatchID).toBe('newGameID');
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
test('when game configuration not supplied, uses previous game config', async () => {
|
|
1234
|
+
const uuid = () => 'newGameID';
|
|
1235
|
+
const app = createApiServer({ db, auth, games, uuid });
|
|
1236
|
+
response = await request(app.callback())
|
|
1237
|
+
.post('/games/foo/1/playAgain')
|
|
1238
|
+
.send('playerID=0&credentials=SECRET1');
|
|
1239
|
+
expect(db.mocks.createMatch).toHaveBeenCalledWith(
|
|
1240
|
+
'newGameID',
|
|
1241
|
+
expect.objectContaining({
|
|
1242
|
+
initialState: expect.objectContaining({
|
|
1243
|
+
ctx: expect.objectContaining({
|
|
1244
|
+
numPlayers: 3,
|
|
1245
|
+
}),
|
|
1246
|
+
}),
|
|
1247
|
+
metadata: expect.objectContaining({
|
|
1248
|
+
setupData: expect.objectContaining({
|
|
1249
|
+
colors: expect.objectContaining({
|
|
1250
|
+
'0': 'green',
|
|
1251
|
+
'1': 'red',
|
|
1252
|
+
}),
|
|
1253
|
+
}),
|
|
1254
|
+
}),
|
|
1255
|
+
})
|
|
1256
|
+
);
|
|
1257
|
+
expect(response.body.nextMatchID).toBe('newGameID');
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
test('fetches next id', async () => {
|
|
1261
|
+
db = new AsyncStorage({
|
|
1262
|
+
fetch: async () => {
|
|
1263
|
+
return {
|
|
1264
|
+
metadata: {
|
|
1265
|
+
players: {
|
|
1266
|
+
'0': {
|
|
1267
|
+
name: 'alice',
|
|
1268
|
+
credentials: 'SECRET1',
|
|
1269
|
+
},
|
|
1270
|
+
'1': {
|
|
1271
|
+
name: 'bob',
|
|
1272
|
+
credentials: 'SECRET2',
|
|
1273
|
+
},
|
|
1274
|
+
},
|
|
1275
|
+
nextMatchID: '12345',
|
|
1276
|
+
},
|
|
1277
|
+
};
|
|
1278
|
+
},
|
|
1279
|
+
});
|
|
1280
|
+
const app = createApiServer({ db, auth, games });
|
|
1281
|
+
response = await request(app.callback())
|
|
1282
|
+
.post('/games/foo/1/playAgain')
|
|
1283
|
+
.send('playerID=0&credentials=SECRET1');
|
|
1284
|
+
expect(response.body.nextMatchID).toBe('12345');
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test('when the match does not exist throws a "not found" error', async () => {
|
|
1288
|
+
db = new AsyncStorage({
|
|
1289
|
+
fetch: async () => ({ metadata: null }),
|
|
1290
|
+
});
|
|
1291
|
+
const app = createApiServer({ db, auth, games });
|
|
1292
|
+
response = await request(app.callback())
|
|
1293
|
+
.post('/games/foo/1/playAgain')
|
|
1294
|
+
.send('playerID=0&playerName=alice');
|
|
1295
|
+
expect(response.status).toEqual(404);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test('when the playerID is undefined throws error 403', async () => {
|
|
1299
|
+
const app = createApiServer({ db, auth, games });
|
|
1300
|
+
response = await request(app.callback())
|
|
1301
|
+
.post('/games/foo/1/playAgain')
|
|
1302
|
+
.send('credentials=SECRET1');
|
|
1303
|
+
expect(response.status).toEqual(403);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
test('when the playerID does not exist throws error 404', async () => {
|
|
1307
|
+
const app = createApiServer({ db, auth, games });
|
|
1308
|
+
response = await request(app.callback())
|
|
1309
|
+
.post('/games/foo/1/playAgain')
|
|
1310
|
+
.send('playerID=3&credentials=SECRET1');
|
|
1311
|
+
expect(response.status).toEqual(404);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
test('when the credentials are invalid throws error 404', async () => {
|
|
1315
|
+
const app = createApiServer({ db, auth, games });
|
|
1316
|
+
response = await request(app.callback())
|
|
1317
|
+
.post('/games/foo/1/playAgain')
|
|
1318
|
+
.send('playerID=0&credentials=SECRET2');
|
|
1319
|
+
expect(response.status).toEqual(403);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
test('when playerID is omitted throws error 403', async () => {
|
|
1323
|
+
const app = createApiServer({ db, auth, games });
|
|
1324
|
+
response = await request(app.callback())
|
|
1325
|
+
.post('/games/foo/1/leave')
|
|
1326
|
+
.send('credentials=foo');
|
|
1327
|
+
expect(response.status).toEqual(403);
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
describe('requesting room list', () => {
|
|
1332
|
+
let db: AsyncStorage;
|
|
1333
|
+
const auth = new Auth();
|
|
1334
|
+
const dbFetch = jest.fn(async (matchID) => {
|
|
1335
|
+
return {
|
|
1336
|
+
metadata: {
|
|
1337
|
+
players: {
|
|
1338
|
+
'0': {
|
|
1339
|
+
id: 0,
|
|
1340
|
+
credentials: 'SECRET1',
|
|
1341
|
+
},
|
|
1342
|
+
'1': {
|
|
1343
|
+
id: 1,
|
|
1344
|
+
credentials: 'SECRET2',
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
unlisted: matchID === 'bar-4',
|
|
1348
|
+
gameover: matchID === 'bar-3' ? { winner: 0 } : undefined,
|
|
1349
|
+
},
|
|
1350
|
+
};
|
|
1351
|
+
});
|
|
1352
|
+
const dblistMatches = jest.fn(async (opts) => {
|
|
1353
|
+
const metadata = {
|
|
1354
|
+
'foo-0': { gameName: 'foo' },
|
|
1355
|
+
'foo-1': { gameName: 'foo' },
|
|
1356
|
+
'bar-2': { gameName: 'bar' },
|
|
1357
|
+
'bar-3': { gameName: 'bar' },
|
|
1358
|
+
'bar-4': { gameName: 'bar' },
|
|
1359
|
+
};
|
|
1360
|
+
const keys = Object.keys(metadata);
|
|
1361
|
+
if (opts && opts.gameName) {
|
|
1362
|
+
return keys.filter((key) => metadata[key].gameName === opts.gameName);
|
|
1363
|
+
}
|
|
1364
|
+
return [...keys];
|
|
1365
|
+
});
|
|
1366
|
+
beforeEach(() => {
|
|
1367
|
+
delete process.env.API_SECRET;
|
|
1368
|
+
db = new AsyncStorage({
|
|
1369
|
+
fetch: dbFetch,
|
|
1370
|
+
listMatches: dblistMatches,
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
describe('when given 2 matches', () => {
|
|
1375
|
+
let response;
|
|
1376
|
+
let matches;
|
|
1377
|
+
beforeEach(async () => {
|
|
1378
|
+
const games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
|
|
1379
|
+
const app = createApiServer({ db, auth, games });
|
|
1380
|
+
response = await request(app.callback()).get('/games/bar');
|
|
1381
|
+
matches = JSON.parse(response.text).matches;
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
test('returns matches for the selected game', async () => {
|
|
1385
|
+
expect(matches).toHaveLength(2);
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
test('returns match ids', async () => {
|
|
1389
|
+
expect(matches[0].matchID).toEqual('bar-2');
|
|
1390
|
+
expect(matches[1].matchID).toEqual('bar-3');
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
test('returns player names', async () => {
|
|
1394
|
+
expect(matches[0].players).toEqual([{ id: 0 }, { id: 1 }]);
|
|
1395
|
+
expect(matches[1].players).toEqual([{ id: 0 }, { id: 1 }]);
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
test('returns gameover data for ended match', async () => {
|
|
1399
|
+
expect(matches[0].gameover).toBeUndefined();
|
|
1400
|
+
expect(matches[1].gameover).toEqual({ winner: 0 });
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
describe('when given filter options', () => {
|
|
1405
|
+
const games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
|
|
1406
|
+
let app;
|
|
1407
|
+
|
|
1408
|
+
beforeEach(() => {
|
|
1409
|
+
app = createApiServer({ db, auth, games });
|
|
1410
|
+
dblistMatches.mockClear();
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
describe('isGameover query param', () => {
|
|
1414
|
+
test('is undefined if not specified in request', async () => {
|
|
1415
|
+
await request(app.callback()).get('/games/bar');
|
|
1416
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1417
|
+
expect.objectContaining({ where: { isGameover: undefined } })
|
|
1418
|
+
);
|
|
1419
|
+
});
|
|
1420
|
+
test('is true', async () => {
|
|
1421
|
+
await request(app.callback()).get('/games/bar?isGameover=true');
|
|
1422
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1423
|
+
expect.objectContaining({ where: { isGameover: true } })
|
|
1424
|
+
);
|
|
1425
|
+
});
|
|
1426
|
+
test('is false', async () => {
|
|
1427
|
+
await request(app.callback()).get('/games/bar?isGameover=false');
|
|
1428
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1429
|
+
expect.objectContaining({ where: { isGameover: false } })
|
|
1430
|
+
);
|
|
1431
|
+
});
|
|
1432
|
+
test('invalid value is ignored', async () => {
|
|
1433
|
+
await request(app.callback()).get('/games/bar?isGameover=5');
|
|
1434
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1435
|
+
expect.objectContaining({ where: { isGameover: undefined } })
|
|
1436
|
+
);
|
|
1437
|
+
});
|
|
1438
|
+
test('uses first array value', async () => {
|
|
1439
|
+
await request(app.callback()).get(
|
|
1440
|
+
'/games/bar?isGameover=true&isGameover=false'
|
|
1441
|
+
);
|
|
1442
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1443
|
+
expect.objectContaining({ where: { isGameover: true } })
|
|
1444
|
+
);
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
describe('updatedBefore query param', () => {
|
|
1449
|
+
test('is undefined if not specified in request', async () => {
|
|
1450
|
+
await request(app.callback()).get('/games/bar');
|
|
1451
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1452
|
+
expect.objectContaining({
|
|
1453
|
+
where: expect.objectContaining({ updatedBefore: undefined }),
|
|
1454
|
+
})
|
|
1455
|
+
);
|
|
1456
|
+
});
|
|
1457
|
+
test('is specified', async () => {
|
|
1458
|
+
const timestamp = new Date(2020, 3, 4, 5, 6, 7);
|
|
1459
|
+
await request(app.callback()).get(
|
|
1460
|
+
`/games/bar?updatedBefore=${timestamp.getTime()}`
|
|
1461
|
+
);
|
|
1462
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1463
|
+
expect.objectContaining({
|
|
1464
|
+
where: expect.objectContaining({
|
|
1465
|
+
updatedBefore: timestamp.getTime(),
|
|
1466
|
+
}),
|
|
1467
|
+
})
|
|
1468
|
+
);
|
|
1469
|
+
});
|
|
1470
|
+
test('invalid value is ignored', async () => {
|
|
1471
|
+
await request(app.callback()).get('/games/bar?updatedBefore=-5');
|
|
1472
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1473
|
+
expect.objectContaining({ where: { updatedBefore: undefined } })
|
|
1474
|
+
);
|
|
1475
|
+
});
|
|
1476
|
+
test('uses first array value', async () => {
|
|
1477
|
+
const t1 = new Date(2020, 3, 4, 5, 6, 7).getTime();
|
|
1478
|
+
const t2 = new Date(2021, 3, 4, 5, 6, 7).getTime();
|
|
1479
|
+
await request(app.callback()).get(
|
|
1480
|
+
`/games/bar?updatedBefore=${t1}&updatedBefore=${t2}`
|
|
1481
|
+
);
|
|
1482
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1483
|
+
expect.objectContaining({
|
|
1484
|
+
where: expect.objectContaining({ updatedBefore: t1 }),
|
|
1485
|
+
})
|
|
1486
|
+
);
|
|
1487
|
+
});
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
describe('updatedAfter query param', () => {
|
|
1491
|
+
test('is undefined if not specified in request', async () => {
|
|
1492
|
+
await request(app.callback()).get('/games/bar');
|
|
1493
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1494
|
+
expect.objectContaining({
|
|
1495
|
+
where: expect.objectContaining({ updatedAfter: undefined }),
|
|
1496
|
+
})
|
|
1497
|
+
);
|
|
1498
|
+
});
|
|
1499
|
+
test('is specified', async () => {
|
|
1500
|
+
const timestamp = new Date(2020, 3, 4, 5, 6, 7);
|
|
1501
|
+
await request(app.callback()).get(
|
|
1502
|
+
`/games/bar?updatedAfter=${timestamp.getTime()}`
|
|
1503
|
+
);
|
|
1504
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1505
|
+
expect.objectContaining({
|
|
1506
|
+
where: expect.objectContaining({
|
|
1507
|
+
updatedAfter: timestamp.getTime(),
|
|
1508
|
+
}),
|
|
1509
|
+
})
|
|
1510
|
+
);
|
|
1511
|
+
});
|
|
1512
|
+
test('invalid value is ignored', async () => {
|
|
1513
|
+
await request(app.callback()).get('/games/bar?updatedAfter=-5');
|
|
1514
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1515
|
+
expect.objectContaining({ where: { updatedAfter: undefined } })
|
|
1516
|
+
);
|
|
1517
|
+
});
|
|
1518
|
+
test('uses first array value', async () => {
|
|
1519
|
+
const t1 = new Date(2020, 3, 4, 5, 6, 7).getTime();
|
|
1520
|
+
const t2 = new Date(2021, 3, 4, 5, 6, 7).getTime();
|
|
1521
|
+
await request(app.callback()).get(
|
|
1522
|
+
`/games/bar?updatedAfter=${t1}&updatedAfter=${t2}`
|
|
1523
|
+
);
|
|
1524
|
+
expect(dblistMatches).toBeCalledWith(
|
|
1525
|
+
expect.objectContaining({
|
|
1526
|
+
where: expect.objectContaining({ updatedAfter: t1 }),
|
|
1527
|
+
})
|
|
1528
|
+
);
|
|
1529
|
+
});
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
describe('requesting room', () => {
|
|
1535
|
+
let db: AsyncStorage;
|
|
1536
|
+
const auth = new Auth();
|
|
1537
|
+
beforeEach(() => {
|
|
1538
|
+
delete process.env.API_SECRET;
|
|
1539
|
+
db = new AsyncStorage({
|
|
1540
|
+
fetch: async () => {
|
|
1541
|
+
return {
|
|
1542
|
+
metadata: {
|
|
1543
|
+
players: {
|
|
1544
|
+
'0': {
|
|
1545
|
+
id: 0,
|
|
1546
|
+
credentials: 'SECRET1',
|
|
1547
|
+
},
|
|
1548
|
+
'1': {
|
|
1549
|
+
id: 1,
|
|
1550
|
+
credentials: 'SECRET2',
|
|
1551
|
+
},
|
|
1552
|
+
},
|
|
1553
|
+
gameover: { winner: 1 },
|
|
1554
|
+
},
|
|
1555
|
+
};
|
|
1556
|
+
},
|
|
1557
|
+
listMatches: async () => {
|
|
1558
|
+
return ['bar:bar-0', 'foo:foo-0', 'bar:bar-1'];
|
|
1559
|
+
},
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
describe('when given room ID', () => {
|
|
1564
|
+
let response;
|
|
1565
|
+
let room;
|
|
1566
|
+
beforeEach(async () => {
|
|
1567
|
+
const games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
|
|
1568
|
+
const app = createApiServer({ db, auth, games });
|
|
1569
|
+
response = await request(app.callback()).get('/games/bar/bar-0');
|
|
1570
|
+
room = JSON.parse(response.text);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
test('returns game ids', async () => {
|
|
1574
|
+
expect(room.matchID).toEqual('bar-0');
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
test('returns player names', async () => {
|
|
1578
|
+
expect(room.players).toEqual([{ id: 0 }, { id: 1 }]);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
test('returns gameover data for ended game', async () => {
|
|
1582
|
+
expect(room.gameover).toEqual({ winner: 1 });
|
|
1583
|
+
});
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
describe('when given a non-existent room ID', () => {
|
|
1587
|
+
let response;
|
|
1588
|
+
beforeEach(async () => {
|
|
1589
|
+
db = new AsyncStorage({
|
|
1590
|
+
fetch: async () => ({ metadata: null }),
|
|
1591
|
+
});
|
|
1592
|
+
const games = [ProcessGameConfig({ name: 'foo' })];
|
|
1593
|
+
const app = createApiServer({ db, auth, games });
|
|
1594
|
+
response = await request(app.callback()).get('/games/bar/doesnotexist');
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
test('throws error 404', async () => {
|
|
1598
|
+
expect(response.status).toEqual(404);
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
describe('when server app is provided', () => {
|
|
1604
|
+
let db: AsyncStorage;
|
|
1605
|
+
const auth = new Auth();
|
|
1606
|
+
let server;
|
|
1607
|
+
let useChain;
|
|
1608
|
+
let games: Game[];
|
|
1609
|
+
|
|
1610
|
+
beforeEach(async () => {
|
|
1611
|
+
useChain = jest.fn(() => ({ use: useChain }));
|
|
1612
|
+
server = { use: useChain };
|
|
1613
|
+
db = new AsyncStorage();
|
|
1614
|
+
games = [
|
|
1615
|
+
{
|
|
1616
|
+
name: 'foo',
|
|
1617
|
+
setup: () => {},
|
|
1618
|
+
},
|
|
1619
|
+
];
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test('call .use method several times', async () => {
|
|
1623
|
+
addApiToServer({ app: server, db, auth, games });
|
|
1624
|
+
expect(server.use.mock.calls.length).toBeGreaterThan(1);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
test('call .use method several times with uuid', async () => {
|
|
1628
|
+
const uuid = () => 'foo';
|
|
1629
|
+
addApiToServer({ app: server, db, auth, games, uuid });
|
|
1630
|
+
expect(server.use.mock.calls.length).toBeGreaterThan(1);
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
describe('cors', () => {
|
|
1635
|
+
const auth = new Auth();
|
|
1636
|
+
const games: Game[] = [];
|
|
1637
|
+
const db = new AsyncStorage();
|
|
1638
|
+
|
|
1639
|
+
describe('no allowed origins', () => {
|
|
1640
|
+
const app = createApiServer({ auth, games, db, origins: false });
|
|
1641
|
+
|
|
1642
|
+
test('does not allow CORS', async () => {
|
|
1643
|
+
const res = await request(app.callback())
|
|
1644
|
+
.get('/games')
|
|
1645
|
+
.set('Origin', 'https://www.example.com')
|
|
1646
|
+
.expect('Vary', 'Origin');
|
|
1647
|
+
expect(res.headers).not.toHaveProperty('access-control-allow-origin');
|
|
1648
|
+
expect(res.headers).not.toHaveProperty('Access-Control-Allow-Origin');
|
|
1649
|
+
});
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
describe('single allowed origin', () => {
|
|
1653
|
+
const origin = 'https://www.example.com';
|
|
1654
|
+
const app = createApiServer({ auth, games, db, origins: origin });
|
|
1655
|
+
|
|
1656
|
+
test('disallows non-matching origin', async () => {
|
|
1657
|
+
const res = await request(app.callback())
|
|
1658
|
+
.get('/games')
|
|
1659
|
+
.set('Origin', 'https://www.other.com')
|
|
1660
|
+
.expect('Vary', 'Origin');
|
|
1661
|
+
expect(res.headers).not.toHaveProperty('access-control-allow-origin');
|
|
1662
|
+
expect(res.headers).not.toHaveProperty('Access-Control-Allow-Origin');
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
// eslint-disable-next-line jest/expect-expect
|
|
1666
|
+
test('allows matching origin', async () => {
|
|
1667
|
+
await request(app.callback())
|
|
1668
|
+
.get('/games')
|
|
1669
|
+
.set('Origin', origin)
|
|
1670
|
+
.expect('Vary', 'Origin')
|
|
1671
|
+
.expect('Access-Control-Allow-Origin', origin);
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
describe('multiple allowed origins', () => {
|
|
1676
|
+
const origins = [Origins.LOCALHOST, 'https://www.example.com'];
|
|
1677
|
+
const app = createApiServer({ auth, games, db, origins });
|
|
1678
|
+
|
|
1679
|
+
test('disallows non-matching origin', async () => {
|
|
1680
|
+
const res = await request(app.callback())
|
|
1681
|
+
.get('/games')
|
|
1682
|
+
.set('Origin', 'https://www.other.com')
|
|
1683
|
+
.expect('Vary', 'Origin');
|
|
1684
|
+
expect(res.headers).not.toHaveProperty('access-control-allow-origin');
|
|
1685
|
+
expect(res.headers).not.toHaveProperty('Access-Control-Allow-Origin');
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
// eslint-disable-next-line jest/expect-expect
|
|
1689
|
+
test('allows matching origin', async () => {
|
|
1690
|
+
const origin = 'http://localhost:5000';
|
|
1691
|
+
await request(app.callback())
|
|
1692
|
+
.get('/games')
|
|
1693
|
+
.set('Origin', origin)
|
|
1694
|
+
.expect('Vary', 'Origin')
|
|
1695
|
+
.expect('Access-Control-Allow-Origin', origin);
|
|
1696
|
+
});
|
|
1697
|
+
});
|
|
1698
|
+
});
|
|
1699
|
+
});
|