Skip to content

Commit 05eacb8

Browse files
authored
Enable adding additional metadata to players in Lobby (#642)
* Add an endpoint to the Lobby to add additional information to a player * Consolidate rename and update endpoint * Update data type to any * Use same handler for rename and update * Fix issues with PR * add test for data when joining
1 parent c51b277 commit 05eacb8

5 files changed

Lines changed: 330 additions & 16 deletions

File tree

docs/documentation/api/Lobby.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,21 @@ Accepts two JSON body parameters, all required:
7474

7575
Returns `playerCredentials` which is the token this player will require to authenticate their actions in the future.
7676

77-
#### Renaming a player
77+
#### Update a player's information
7878

79-
##### POST `/games/{name}/{id}/rename`
79+
##### POST `/games/{name}/{id}/update`
8080

81-
Rename a user in the room instance `id` of a game named `name` previously joined by the player.
81+
Rename and/or update additional information of a user in the room instance `id` of a game named `name` previously joined by the player.
8282

83-
Accepts three parameters, all required:
83+
Accepts four parameters, requires at least one of the two optional parameters:
8484

85-
`playerID`: the ID used by the player in the game (0, 1...).
85+
`playerID` (required): the ID used by the player in the game (0,1...).
8686

87-
`credentials`: the authentication token of the player.
87+
`crendentials` (required): the authentication token of the player.
88+
89+
`newName` (optional): the new name of the player.
8890

89-
`newName`: the new name of the player.
91+
`data` (optional): additional information associated to the player.
9092

9193
#### Leaving a room
9294

src/master/master.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ export class Master {
334334

335335
if (gameMetadata) {
336336
filteredMetadata = Object.values(gameMetadata.players).map(player => {
337-
return { id: player.id, name: player.name };
337+
const { credentials, ...filteredData } = player;
338+
return filteredData;
338339
});
339340
}
340341

src/server/api.test.ts

Lines changed: 288 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ describe('.createApiServer', () => {
258258
});
259259
response = await request(app.callback())
260260
.post('/games/foo/1/join')
261-
.send('playerID=0&playerName=alice');
261+
.send({ playerID: 0, playerName: 'alice', data: 99 });
262262
});
263263

264264
test('is successful', async () => {
@@ -281,6 +281,18 @@ describe('.createApiServer', () => {
281281
})
282282
);
283283
});
284+
test('updates the player data', async () => {
285+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
286+
'1',
287+
expect.objectContaining({
288+
players: expect.objectContaining({
289+
'0': expect.objectContaining({
290+
data: 99,
291+
}),
292+
}),
293+
})
294+
);
295+
});
284296
});
285297

286298
describe('when the playerID does not exist', () => {
@@ -353,7 +365,7 @@ describe('.createApiServer', () => {
353365
});
354366
});
355367

356-
describe('rename', () => {
368+
describe('rename with deprecated endpoint', () => {
357369
let response;
358370
let db;
359371
let games;
@@ -407,6 +419,18 @@ describe('.createApiServer', () => {
407419
.send('playerID=0&credentials=SECRET1&newName=ali');
408420
});
409421

422+
describe('when the playerName is not a string', () => {
423+
test('throws newName must be a string', async () => {
424+
const app = createApiServer({ db, games });
425+
response = await request(app.callback())
426+
.post('/games/foo/1/rename')
427+
.send({ playerID: 0, credentials: 'SECRET1', newName: 2 });
428+
expect(response.text).toEqual(
429+
'newName must be a string, got number'
430+
);
431+
});
432+
});
433+
410434
test('is successful', async () => {
411435
expect(response.status).toEqual(200);
412436
});
@@ -472,6 +496,268 @@ describe('.createApiServer', () => {
472496
});
473497
});
474498

499+
describe('rename with update endpoint', () => {
500+
let response;
501+
let db;
502+
let games;
503+
504+
beforeEach(() => {
505+
games = [ProcessGameConfig({ name: 'foo' })];
506+
});
507+
508+
describe('for an unprotected lobby', () => {
509+
beforeEach(() => {
510+
delete process.env.API_SECRET;
511+
});
512+
513+
describe('when the game does not exist', () => {
514+
test('throws game not found', async () => {
515+
db = new AsyncStorage({
516+
fetch: async () => ({ metadata: null }),
517+
});
518+
const app = createApiServer({ db, games });
519+
response = await request(app.callback())
520+
.post('/games/foo/1/update')
521+
.send('playerID=0&playerName=alice&newName=ali');
522+
expect(response.text).toEqual('Game 1 not found');
523+
});
524+
});
525+
526+
describe('when the game does exist', () => {
527+
describe('when the playerID does exist', () => {
528+
beforeEach(async () => {
529+
db = new AsyncStorage({
530+
fetch: async () => {
531+
return {
532+
metadata: {
533+
players: {
534+
'0': {
535+
name: 'alice',
536+
credentials: 'SECRET1',
537+
},
538+
'1': {
539+
name: 'bob',
540+
credentials: 'SECRET2',
541+
},
542+
},
543+
},
544+
};
545+
},
546+
});
547+
const app = createApiServer({ db, games });
548+
response = await request(app.callback())
549+
.post('/games/foo/1/update')
550+
.send('playerID=0&credentials=SECRET1&newName=ali');
551+
});
552+
553+
describe('when the playerName is not a string', () => {
554+
test('throws newName must be a string', async () => {
555+
const app = createApiServer({ db, games });
556+
response = await request(app.callback())
557+
.post('/games/foo/1/update')
558+
.send({ playerID: 0, credentials: 'SECRET1', newName: 2 });
559+
expect(response.text).toEqual(
560+
'newName must be a string, got number'
561+
);
562+
});
563+
});
564+
565+
test('is successful', async () => {
566+
expect(response.status).toEqual(200);
567+
});
568+
569+
test('updates the players', async () => {
570+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
571+
'1',
572+
expect.objectContaining({
573+
players: expect.objectContaining({
574+
'0': expect.objectContaining({
575+
name: 'ali',
576+
}),
577+
}),
578+
})
579+
);
580+
});
581+
});
582+
583+
describe('when the playerID does not exist', () => {
584+
test('throws player not found', async () => {
585+
const app = createApiServer({ db, games });
586+
response = await request(app.callback())
587+
.post('/games/foo/1/update')
588+
.send('playerID=2&credentials=SECRET1&newName=joe');
589+
expect(response.text).toEqual('Player 2 not found');
590+
});
591+
});
592+
593+
describe('when the credentials are invalid', () => {
594+
test('throws invalid credentials', async () => {
595+
const app = createApiServer({ db, games });
596+
response = await request(app.callback())
597+
.post('/games/foo/1/update')
598+
.send('playerID=0&credentials=SECRET2&newName=mike');
599+
expect(response.text).toEqual('Invalid credentials SECRET2');
600+
});
601+
});
602+
describe('when playerID is omitted', () => {
603+
beforeEach(async () => {
604+
const app = createApiServer({ db, games });
605+
response = await request(app.callback())
606+
.post('/games/foo/1/update')
607+
.send('credentials=foo&newName=bill');
608+
});
609+
test('throws playerID is required', async () => {
610+
expect(response.text).toEqual('playerID is required');
611+
});
612+
describe('when newName is omitted', () => {
613+
beforeEach(async () => {
614+
const app = createApiServer({ db, games });
615+
response = await request(app.callback())
616+
.post('/games/foo/1/update')
617+
.send('credentials=foo&playerID=0');
618+
});
619+
620+
test('throws newName is required', async () => {
621+
expect(response.text).toEqual('newName or data is required');
622+
});
623+
});
624+
});
625+
});
626+
});
627+
});
628+
629+
describe('updating player metadata', () => {
630+
let response;
631+
let db;
632+
let games;
633+
634+
beforeEach(() => {
635+
games = [ProcessGameConfig({ name: 'foo' })];
636+
});
637+
638+
describe('for an unprotected lobby', () => {
639+
beforeEach(() => {
640+
delete process.env.API_SECRET;
641+
});
642+
describe('when the game does not exist', () => {
643+
test('throws game not found', async () => {
644+
db = new AsyncStorage({
645+
fetch: async () => ({ metadata: null }),
646+
});
647+
const app = createApiServer({ db, games });
648+
response = await request(app.callback())
649+
.post('/games/foo/1/update')
650+
.send({ playerID: 0, data: { subdata: 'text' } });
651+
expect(response.text).toEqual('Game 1 not found');
652+
});
653+
});
654+
655+
describe('when the game does exist', () => {
656+
describe('when the playerID does exist', () => {
657+
beforeEach(async () => {
658+
db = new AsyncStorage({
659+
fetch: async () => {
660+
return {
661+
metadata: {
662+
players: {
663+
'0': {
664+
name: 'alice',
665+
credentials: 'SECRET1',
666+
},
667+
'1': {
668+
name: 'bob',
669+
credentials: 'SECRET2',
670+
},
671+
},
672+
},
673+
};
674+
},
675+
});
676+
const app = createApiServer({ db, games });
677+
response = await request(app.callback())
678+
.post('/games/foo/1/update')
679+
.send({
680+
playerID: 0,
681+
credentials: 'SECRET1',
682+
data: { subdata: 'text' },
683+
});
684+
});
685+
686+
test('is successful', async () => {
687+
expect(response.status).toEqual(200);
688+
});
689+
690+
test('updates the players', async () => {
691+
expect(db.mocks.setMetadata).toHaveBeenCalledWith(
692+
'1',
693+
expect.objectContaining({
694+
players: expect.objectContaining({
695+
'0': expect.objectContaining({
696+
data: expect.objectContaining({
697+
subdata: 'text',
698+
}),
699+
}),
700+
}),
701+
})
702+
);
703+
});
704+
});
705+
706+
describe('when the playerID does not exist', () => {
707+
test('throws playerID not found', async () => {
708+
const app = createApiServer({ db, games });
709+
response = await request(app.callback())
710+
.post('/games/foo/1/update')
711+
.send({
712+
playerID: 2,
713+
credentials: 'SECRET1',
714+
data: { subdata: 'text' },
715+
});
716+
expect(response.text).toEqual('Player 2 not found');
717+
});
718+
});
719+
720+
describe('when the credentials are invalid', () => {
721+
test('invalid credentials', async () => {
722+
const app = createApiServer({ db, games });
723+
response = await request(app.callback())
724+
.post('/games/foo/1/update')
725+
.send({
726+
playerID: 0,
727+
credentials: 'SECRET2',
728+
data: { subdata: 'text' },
729+
});
730+
expect(response.text).toEqual('Invalid credentials SECRET2');
731+
});
732+
});
733+
describe('when playerID is omitted', () => {
734+
beforeEach(async () => {
735+
const app = createApiServer({ db, games });
736+
response = await request(app.callback())
737+
.post('/games/foo/1/update')
738+
.send({ credentials: 'foo', data: { subdata: 'text' } });
739+
});
740+
741+
test('throws playerID is required', async () => {
742+
expect(response.text).toEqual('playerID is required');
743+
});
744+
describe('when data is omitted', () => {
745+
beforeEach(async () => {
746+
const app = createApiServer({ db, games });
747+
response = await request(app.callback())
748+
.post('/games/foo/1/update')
749+
.send({ playerID: 0, credentials: 'foo' });
750+
});
751+
752+
test('throws data is required', async () => {
753+
expect(response.text).toEqual('newName or data is required');
754+
});
755+
});
756+
});
757+
});
758+
});
759+
});
760+
475761
describe('leaving a room', () => {
476762
let response;
477763
let db;

0 commit comments

Comments
 (0)