Skip to content

Commit 0cfe756

Browse files
committed
Add RESTful get/update retro API endpoints [#19], fix not booting users from load-balanced instances when changing password [#42]
1 parent 8e7edc4 commit 0cfe756

File tree

4 files changed

+260
-102
lines changed

4 files changed

+260
-102
lines changed

backend/src/api-tests/retros.test.ts

Lines changed: 203 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -149,88 +149,231 @@ describe('API retros', () => {
149149
});
150150
});
151151

152-
describe('PUT /api/retros/retro-id/password', () => {
153-
it('changes the retro password', async (props) => {
152+
describe('GET /api/retros/retro-id', () => {
153+
it('returns the current retro state', async (props) => {
154154
const { server, hooks } = props.getTyped(PROPS);
155155

156156
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
157-
const grant1 = await hooks.retroAuthService.grantForPassword(
157+
const retroToken = await getRetroToken(hooks, id);
158+
159+
const response = await request(server)
160+
.get(`/api/retros/${id}`)
161+
.set('Authorization', `Bearer ${retroToken}`)
162+
.expect(200);
163+
164+
expect(response.body).toEqual({
158165
id,
159-
'password1',
160-
);
161-
if (!grant1) {
162-
throw new Error('failed to get retro token');
163-
}
166+
slug: 'my-retro',
167+
name: 'My Retro',
168+
ownerId: 'nobody',
169+
state: {},
170+
groupStates: {},
171+
format: 'mood',
172+
options: {},
173+
items: [],
174+
});
175+
});
164176

165-
await request(server)
166-
.put(`/api/retros/${id}/password`)
167-
.send({ password: 'password2', evictUsers: false })
168-
.set('Authorization', `Bearer ${grant1.token}`)
169-
.expect(200)
170-
.expect('Content-Type', /application\/json/);
177+
it('responds HTTP Unauthorized if no credentials are given', async (props) => {
178+
const { server, hooks } = props.getTyped(PROPS);
171179

172-
// existing token is still valid
173-
expect(
174-
await hooks.retroAuthService.readAndVerifyToken(id, grant1.token),
175-
).isTruthy();
180+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
181+
await request(server).get(`/api/retros/${id}`).expect(401);
182+
});
176183

177-
// new token requests must use new password
178-
expect(
179-
await hooks.retroAuthService.grantForPassword(id, 'password1'),
180-
).isNull();
184+
it('responds HTTP Unauthorized if credentials are incorrect', async (props) => {
185+
const { server, hooks } = props.getTyped(PROPS);
181186

182-
const grant2 = await hooks.retroAuthService.grantForPassword(
183-
id,
184-
'password2',
185-
);
186-
expect(grant2).isTruthy();
187-
expect(
188-
await hooks.retroAuthService.readAndVerifyToken(id, grant2!.token),
189-
).isTruthy();
187+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
188+
await request(server)
189+
.get(`/api/retros/${id}`)
190+
.set('Authorization', 'Bearer Foo')
191+
.expect(401);
190192
});
191193

192-
it('voids existing tokens if requested', async (props) => {
194+
it('responds HTTP Forbidden if scope is not "read"', async (props) => {
193195
const { server, hooks } = props.getTyped(PROPS);
194196

195197
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
196-
const grant1 = await hooks.retroAuthService.grantForPassword(
197-
id,
198-
'password1',
199-
);
200-
if (!grant1) {
201-
throw new Error('failed to get retro token');
202-
}
198+
const retroToken = await getRetroToken(hooks, id, {
199+
read: false,
200+
});
203201

204202
await request(server)
205-
.put(`/api/retros/${id}/password`)
206-
.send({ password: 'password2', evictUsers: true })
207-
.set('Authorization', `Bearer ${grant1.token}`)
208-
.expect(200)
209-
.expect('Content-Type', /application\/json/);
203+
.get(`/api/retros/${id}`)
204+
.set('Authorization', `Bearer ${retroToken}`)
205+
.expect(403);
206+
});
207+
});
210208

211-
// existing token is no longer valid
212-
expect(
213-
await hooks.retroAuthService.readAndVerifyToken(id, grant1.token),
214-
).isNull();
209+
describe('PATCH /api/retros/retro-id', () => {
210+
describe('with retro spec', () => {
211+
it('applies the spec to the retro', async (props) => {
212+
const { server, hooks } = props.getTyped(PROPS);
215213

216-
// new tokens are valid
217-
const grant2 = await hooks.retroAuthService.grantForPassword(
218-
id,
219-
'password2',
220-
);
221-
expect(grant2).isTruthy();
222-
expect(
223-
await hooks.retroAuthService.readAndVerifyToken(id, grant2!.token),
224-
).isTruthy();
214+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
215+
const retroToken = await getRetroToken(hooks, id);
216+
217+
await request(server)
218+
.patch(`/api/retros/${id}`)
219+
.send({ change: { state: ['=', { foo: 'bar' }] } })
220+
.set('Authorization', `Bearer ${retroToken}`)
221+
.expect(200);
222+
223+
const retro = await hooks.retroService.getRetro(id);
224+
expect(retro?.state).toEqual({ foo: 'bar' });
225+
});
226+
227+
it('does not require "manage" scope', async (props) => {
228+
const { server, hooks } = props.getTyped(PROPS);
229+
230+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
231+
const retroToken = await getRetroToken(hooks, id, {
232+
manage: false,
233+
});
234+
235+
await request(server)
236+
.patch(`/api/retros/${id}`)
237+
.send({ change: { state: ['=', { foo: 'bar' }] } })
238+
.set('Authorization', `Bearer ${retroToken}`)
239+
.expect(200);
240+
241+
const retro = await hooks.retroService.getRetro(id);
242+
expect(retro?.state).toEqual({ foo: 'bar' });
243+
});
244+
245+
it('responds HTTP Forbidden if scope is not "write"', async (props) => {
246+
const { server, hooks } = props.getTyped(PROPS);
247+
248+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
249+
const retroToken = await getRetroToken(hooks, id, {
250+
write: false,
251+
});
252+
253+
await request(server)
254+
.patch(`/api/retros/${id}`)
255+
.send({ change: { state: ['=', { foo: 'bar' }] } })
256+
.set('Authorization', `Bearer ${retroToken}`)
257+
.expect(403);
258+
259+
const retro = await hooks.retroService.getRetro(id);
260+
expect(retro?.state).toEqual({});
261+
});
262+
});
263+
264+
describe('with new password', () => {
265+
it('changes the retro password', async (props) => {
266+
const { server, hooks } = props.getTyped(PROPS);
267+
268+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
269+
const grant1 = await hooks.retroAuthService.grantForPassword(
270+
id,
271+
'password1',
272+
);
273+
if (!grant1) {
274+
throw new Error('failed to get retro token');
275+
}
276+
277+
await request(server)
278+
.patch(`/api/retros/${id}`)
279+
.send({ setPassword: { password: 'password2', evictUsers: false } })
280+
.set('Authorization', `Bearer ${grant1.token}`)
281+
.expect(200)
282+
.expect('Content-Type', /application\/json/);
283+
284+
// existing token is still valid
285+
expect(
286+
await hooks.retroAuthService.readAndVerifyToken(id, grant1.token),
287+
).isTruthy();
288+
289+
// new token requests must use new password
290+
expect(
291+
await hooks.retroAuthService.grantForPassword(id, 'password1'),
292+
).isNull();
293+
294+
const grant2 = await hooks.retroAuthService.grantForPassword(
295+
id,
296+
'password2',
297+
);
298+
expect(grant2).isTruthy();
299+
expect(
300+
await hooks.retroAuthService.readAndVerifyToken(id, grant2!.token),
301+
).isTruthy();
302+
});
303+
304+
it('voids existing tokens if requested', async (props) => {
305+
const { server, hooks } = props.getTyped(PROPS);
306+
307+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
308+
const grant1 = await hooks.retroAuthService.grantForPassword(
309+
id,
310+
'password1',
311+
);
312+
if (!grant1) {
313+
throw new Error('failed to get retro token');
314+
}
315+
316+
await request(server)
317+
.patch(`/api/retros/${id}`)
318+
.send({ setPassword: { password: 'password2', evictUsers: true } })
319+
.set('Authorization', `Bearer ${grant1.token}`)
320+
.expect(200)
321+
.expect('Content-Type', /application\/json/);
322+
323+
// existing token is no longer valid
324+
expect(
325+
await hooks.retroAuthService.readAndVerifyToken(id, grant1.token),
326+
).isNull();
327+
328+
// new tokens are valid
329+
const grant2 = await hooks.retroAuthService.grantForPassword(
330+
id,
331+
'password2',
332+
);
333+
expect(grant2).isTruthy();
334+
expect(
335+
await hooks.retroAuthService.readAndVerifyToken(id, grant2!.token),
336+
).isTruthy();
337+
});
338+
339+
it('does not require "write" scope', async (props) => {
340+
const { server, hooks } = props.getTyped(PROPS);
341+
342+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
343+
const retroToken = await getRetroToken(hooks, id, {
344+
write: false,
345+
});
346+
347+
await request(server)
348+
.patch(`/api/retros/${id}`)
349+
.send({ setPassword: { password: 'password2', evictUsers: false } })
350+
.set('Authorization', `Bearer ${retroToken}`)
351+
.expect(200);
352+
});
353+
354+
it('responds HTTP Forbidden if scope is not "manage"', async (props) => {
355+
const { server, hooks } = props.getTyped(PROPS);
356+
357+
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
358+
const retroToken = await getRetroToken(hooks, id, {
359+
manage: false,
360+
});
361+
362+
await request(server)
363+
.patch(`/api/retros/${id}`)
364+
.send({ setPassword: { password: 'password2', evictUsers: false } })
365+
.set('Authorization', `Bearer ${retroToken}`)
366+
.expect(403);
367+
});
225368
});
226369

227370
it('responds HTTP Unauthorized if no credentials are given', async (props) => {
228371
const { server, hooks } = props.getTyped(PROPS);
229372

230373
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
231374
await request(server)
232-
.put(`/api/retros/${id}/password`)
233-
.send({ password: 'password2', evictUsers: false })
375+
.patch(`/api/retros/${id}`)
376+
.send({ setPassword: { password: 'password2', evictUsers: false } })
234377
.expect(401);
235378
});
236379

@@ -239,26 +382,11 @@ describe('API retros', () => {
239382

240383
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
241384
await request(server)
242-
.put(`/api/retros/${id}/password`)
243-
.send({ password: 'password2', evictUsers: false })
385+
.patch(`/api/retros/${id}`)
386+
.send({ setPassword: { password: 'password2', evictUsers: false } })
244387
.set('Authorization', 'Bearer Foo')
245388
.expect(401);
246389
});
247-
248-
it('responds HTTP Forbidden if scope is not "manage"', async (props) => {
249-
const { server, hooks } = props.getTyped(PROPS);
250-
251-
const id = (await hooks.retroService.getRetroIdForSlug('my-retro'))!;
252-
const retroToken = await getRetroToken(hooks, id, {
253-
manage: false,
254-
});
255-
256-
await request(server)
257-
.put(`/api/retros/${id}/password`)
258-
.send({ password: 'password2', evictUsers: false })
259-
.set('Authorization', `Bearer ${retroToken}`)
260-
.expect(403);
261-
});
262390
});
263391
});
264392

0 commit comments

Comments
 (0)