Skip to content

Commit

Permalink
feat(server): allow to filter listGames query (#740)
Browse files Browse the repository at this point in the history
* add createdAt, updatedAt to metadata, add listGames filters

* case-insensitive boolean parsing

* implement inmemory listGames filter
  • Loading branch information
janKir authored Jun 14, 2020
1 parent 693142a commit 3c8777b
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 59 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"husky": "^1.3.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.0.0",
"jest-date-mock": "^1.0.8",
"jest-transform-svelte": "^2.1.0",
"lint-staged": "^8.1.0",
"node-persist": "^3.0.4",
Expand Down Expand Up @@ -174,7 +175,8 @@
"src/types.ts"
],
"setupFiles": [
"raf/polyfill"
"raf/polyfill",
"jest-date-mock"
],
"setupFilesAfterEnv": [
"@testing-library/jest-dom/extend-expect"
Expand Down Expand Up @@ -211,4 +213,4 @@
"pre-push": "npm run test:coverage"
}
}
}
}
54 changes: 52 additions & 2 deletions src/master/master.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ import {
import { error } from '../core/logger';
import { Server } from '../types';
import * as StorageAPI from '../server/db/base';
import * as dateMock from 'jest-date-mock';

jest.mock('../core/logger', () => ({
info: jest.fn(),
error: jest.fn(),
}));

beforeEach(() => {
dateMock.clear();
});

class InMemoryAsync extends InMemory {
type() {
return StorageAPI.Type.ASYNC;
Expand Down Expand Up @@ -81,6 +86,8 @@ describe('sync', () => {
name: 'Bob',
},
},
createdAt: 0,
updatedAt: 0,
};
db.setMetadata('gameID', dbMetadata);
const masterWithMetadata = new Master(game, db, TransportAPI(send));
Expand Down Expand Up @@ -216,6 +223,8 @@ describe('update', () => {
gameName: 'tic-tac-toe',
setupData: {},
players: { '0': { id: 0 }, '1': { id: 1 } },
createdAt: 0,
updatedAt: 0,
};
db.setMetadata(id, dbMetadata);
const masterWithMetadata = new Master(game, db, TransportAPI(send));
Expand All @@ -235,6 +244,31 @@ describe('update', () => {
gameName: 'tic-tac-toe',
setupData: {},
players: { '0': { id: 0 }, '1': { id: 1 } },
createdAt: 0,
updatedAt: 0,
};
db.setMetadata(id, dbMetadata);
const masterWithMetadata = new Master(game, db, TransportAPI(send));
await masterWithMetadata.onSync(id, '0', 2);

const updatedAt = new Date(2020, 3, 4, 5, 6, 7);
dateMock.advanceTo(updatedAt);
const gameOverArg = 'gameOverArg';
const event = ActionCreators.gameEvent('endGame', gameOverArg);
await masterWithMetadata.onUpdate(event, 0, id, '0');
const { metadata } = db.fetch(id, { metadata: true });
expect(metadata.updatedAt).toEqual(updatedAt.getTime());
});

test('writes updatedAt to metadata with async storage API', async () => {
const id = 'gameWithMetadata';
const db = new InMemoryAsync();
const dbMetadata = {
gameName: 'tic-tac-toe',
setupData: {},
players: { '0': { id: 0 }, '1': { id: 1 } },
createdAt: 0,
updatedAt: 0,
};
db.setMetadata(id, dbMetadata);
const masterWithMetadata = new Master(game, db, TransportAPI(send));
Expand Down Expand Up @@ -600,7 +634,13 @@ describe('getPlayerMetadata', () => {
test('then playerMetadata is undefined', () => {
expect(
getPlayerMetadata(
{ gameName: '', setupData: {}, players: { '1': { id: 1 } } },
{
gameName: '',
setupData: {},
players: { '1': { id: 1 } },
createdAt: 0,
updatedAt: 0,
},
'0'
)
).toBeUndefined();
Expand All @@ -611,7 +651,13 @@ describe('getPlayerMetadata', () => {
test('then playerMetadata is returned', () => {
const playerMetadata = { id: 0, credentials: 'SECRET' };
const result = getPlayerMetadata(
{ gameName: '', setupData: {}, players: { '0': playerMetadata } },
{
gameName: '',
setupData: {},
players: { '0': playerMetadata },
createdAt: 0,
updatedAt: 0,
},
'0'
);
expect(result).toBe(playerMetadata);
Expand All @@ -635,6 +681,8 @@ describe('doesMatchRequireAuthentication', () => {
players: {
'0': { id: 1 },
},
createdAt: 0,
updatedAt: 0,
};
const result = doesMatchRequireAuthentication(matchMetadata);
expect(result).toBe(false);
Expand All @@ -652,6 +700,8 @@ describe('doesMatchRequireAuthentication', () => {
credentials: 'SECRET',
},
},
createdAt: 0,
updatedAt: 0,
};
const result = doesMatchRequireAuthentication(matchMetadata);
expect(result).toBe(true);
Expand Down
11 changes: 5 additions & 6 deletions src/master/master.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,14 @@ export class Master {
const { deltalog, ...stateWithoutDeltalog } = state;

let newMetadata: Server.MatchMetadata | undefined;
if (
metadata &&
!('gameover' in metadata) &&
state.ctx.gameover !== undefined
) {
if (metadata && !('gameover' in metadata)) {
newMetadata = {
...metadata,
gameover: state.ctx.gameover,
updatedAt: Date.now(),
};
if (state.ctx.gameover !== undefined) {
newMetadata.gameover = state.ctx.gameover;
}
}

if (IsSynchronous(this.storageAPI)) {
Expand Down
155 changes: 123 additions & 32 deletions src/server/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import request from 'supertest';
import Koa from 'koa';
import * as dateMock from 'jest-date-mock';

import { createRouter, configureApp } from './api';
import { ProcessGameConfig } from '../core/game';
Expand All @@ -16,6 +17,10 @@ import { Game } from '../types';

jest.setTimeout(2000000000);

beforeEach(() => {
dateMock.clear();
});

type StorageMocks = Record<
'createGame' | 'setState' | 'fetch' | 'setMetadata' | 'listGames' | 'wipe',
jest.Mock | ((...args: any[]) => any)
Expand Down Expand Up @@ -83,6 +88,7 @@ describe('.createRouter', () => {
let app: Koa;
let db: AsyncStorage;
let games: Game[];
const updatedAt = new Date(2020, 3, 4, 5, 6, 7);

beforeEach(async () => {
db = new AsyncStorage();
Expand All @@ -101,6 +107,8 @@ describe('.createRouter', () => {

describe('for an unprotected lobby server', () => {
beforeEach(async () => {
dateMock.advanceTo(updatedAt);

delete process.env.API_SECRET;

const uuid = () => 'matchID';
Expand Down Expand Up @@ -131,6 +139,8 @@ describe('.createRouter', () => {
'1': expect.objectContaining({}),
}),
unlisted: false,
createdAt: updatedAt.getTime(),
updatedAt: updatedAt.getTime(),
}),
})
);
Expand Down Expand Up @@ -1183,41 +1193,43 @@ describe('.createRouter', () => {

describe('requesting room list', () => {
let db: AsyncStorage;
const dbFetch = jest.fn(async matchID => {
return {
metadata: {
players: {
'0': {
id: 0,
credentials: 'SECRET1',
},
'1': {
id: 1,
credentials: 'SECRET2',
},
},
unlisted: matchID === 'bar-4',
gameover: matchID === 'bar-3' ? { winner: 0 } : undefined,
},
};
});
const dbListGames = jest.fn(async opts => {
const metadata = {
'foo-0': { gameName: 'foo' },
'foo-1': { gameName: 'foo' },
'bar-2': { gameName: 'bar' },
'bar-3': { gameName: 'bar' },
'bar-4': { gameName: 'bar' },
};
const keys = Object.keys(metadata);
if (opts && opts.gameName) {
return keys.filter(key => metadata[key].gameName === opts.gameName);
}
return [...keys];
});
beforeEach(() => {
delete process.env.API_SECRET;
db = new AsyncStorage({
fetch: async matchID => {
return {
metadata: {
players: {
'0': {
id: 0,
credentials: 'SECRET1',
},
'1': {
id: 1,
credentials: 'SECRET2',
},
},
unlisted: matchID === 'bar-4',
gameover: matchID === 'bar-3' ? { winner: 0 } : undefined,
},
};
},
listGames: async opts => {
const metadata = {
'foo-0': { gameName: 'foo' },
'foo-1': { gameName: 'foo' },
'bar-2': { gameName: 'bar' },
'bar-3': { gameName: 'bar' },
'bar-4': { gameName: 'bar' },
};
const keys = Object.keys(metadata);
if (opts && opts.gameName) {
return keys.filter(key => metadata[key].gameName === opts.gameName);
}
return [...keys];
},
fetch: dbFetch,
listGames: dbListGames,
});
});

Expand Down Expand Up @@ -1250,6 +1262,85 @@ describe('.createRouter', () => {
expect(matches[1].gameover).toEqual({ winner: 0 });
});
});

describe('when given filter options', () => {
const games = [ProcessGameConfig({ name: 'foo' }), { name: 'bar' }];
let app;

beforeEach(() => {
app = createApiServer({ db, games });
dbListGames.mockClear();
});

describe('isGameover query param', () => {
test('is undefined if not specified in request', async () => {
await request(app.callback()).get('/games/bar');
expect(dbListGames).toBeCalledWith(
expect.objectContaining({ where: { isGameover: undefined } })
);
});
test('is true', async () => {
await request(app.callback()).get('/games/bar?isGameover=true');
expect(dbListGames).toBeCalledWith(
expect.objectContaining({ where: { isGameover: true } })
);
});
test('is false', async () => {
await request(app.callback()).get('/games/bar?isGameover=false');
expect(dbListGames).toBeCalledWith(
expect.objectContaining({ where: { isGameover: false } })
);
});
});

describe('updatedBefore query param', () => {
test('is undefined if not specified in request', async () => {
await request(app.callback()).get('/games/bar');
expect(dbListGames).toBeCalledWith(
expect.objectContaining({
where: expect.objectContaining({ updatedBefore: undefined }),
})
);
});
test('is specified', async () => {
const timestamp = new Date(2020, 3, 4, 5, 6, 7);
await request(app.callback()).get(
`/games/bar?updatedBefore=${timestamp.getTime()}`
);
expect(dbListGames).toBeCalledWith(
expect.objectContaining({
where: expect.objectContaining({
updatedBefore: timestamp.getTime(),
}),
})
);
});
});

describe('updatedAfter query param', () => {
test('is undefined if not specified in request', async () => {
await request(app.callback()).get('/games/bar');
expect(dbListGames).toBeCalledWith(
expect.objectContaining({
where: expect.objectContaining({ updatedAfter: undefined }),
})
);
});
test('is specified', async () => {
const timestamp = new Date(2020, 3, 4, 5, 6, 7);
await request(app.callback()).get(
`/games/bar?updatedAfter=${timestamp.getTime()}`
);
expect(dbListGames).toBeCalledWith(
expect.objectContaining({
where: expect.objectContaining({
updatedAfter: timestamp.getTime(),
}),
})
);
});
});
});
});

describe('requesting room', () => {
Expand Down
Loading

0 comments on commit 3c8777b

Please sign in to comment.