Skip to content

Commit

Permalink
Add tests for deleting chat messages, use HTTP endpoints for mute test (
Browse files Browse the repository at this point in the history
#665)

* Use mute endpoint in chat test

* order

* reduce interval

* Add tests for deleting messages

* use retryFor in booth vote test too

* factor out retry, i guess
  • Loading branch information
goto-bus-stop authored Nov 25, 2024
1 parent b882eb2 commit 4ec71f3
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 14 deletions.
15 changes: 10 additions & 5 deletions test/booth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import delay from 'delay';
import supertest from 'supertest';
import createUwave from './utils/createUwave.mjs';
import testSource from './utils/testSource.mjs';
import { retryFor } from './utils/retry.mjs';

describe('Booth', () => {
describe('GET /booth', () => {
Expand Down Expand Up @@ -149,9 +150,10 @@ describe('Booth', () => {
.set('Cookie', `uwsession=${token}`)
.send({ direction: -1 })
.expect(200);
await delay(200);

assert(receivedMessages.some((message) => message.command === 'vote' && message.data.value === -1));
await retryFor(500, () => {
assert(receivedMessages.some((message) => message.command === 'vote' && message.data.value === -1));
});

// Resubmit vote without changing
receivedMessages.length = 0;
Expand All @@ -160,8 +162,10 @@ describe('Booth', () => {
.set('Cookie', `uwsession=${token}`)
.send({ direction: -1 })
.expect(200);
await delay(200);

// Need to just wait, as we can't assert for the absence of something happening
// without waiting the whole time limit
await delay(200);
assert(
!receivedMessages.some((message) => message.command === 'vote' && message.data.value === -1),
'should not have re-emitted the vote',
Expand All @@ -172,9 +176,10 @@ describe('Booth', () => {
.set('Cookie', `uwsession=${token}`)
.send({ direction: 1 })
.expect(200);
await delay(200);

assert(receivedMessages.some((message) => message.command === 'vote' && message.data.value === 1));
await retryFor(500, () => {
assert(receivedMessages.some((message) => message.command === 'vote' && message.data.value === 1));
});

djWs.close();
});
Expand Down
189 changes: 180 additions & 9 deletions test/chat.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { randomUUID } from 'crypto';
import assert from 'assert';
import * as sinon from 'sinon';
import delay from 'delay';
import supertest from 'supertest';
import createUwave from './utils/createUwave.mjs';
import { retryFor } from './utils/retry.mjs';

const sandbox = sinon.createSandbox();

Expand All @@ -27,18 +29,23 @@ describe('Chat', () => {
});

ws.send(JSON.stringify({ command: 'sendChat', data: 'Message text' }));
await delay(500);

assert(receivedMessages.some((message) => message.command === 'chatMessage' && message.data.userID === user.id && message.data.message === 'Message text'));
await retryFor(1500, () => {
assert(receivedMessages.some((message) => message.command === 'chatMessage' && message.data.userID === user.id && message.data.message === 'Message text'));
});
});

it('does not broadcast chat messages from muted users', async () => {
const user = await uw.test.createUser();
const token = await uw.test.createTestSessionToken(user);
await uw.acl.allow(user, ['admin']);
const mutedUser = await uw.test.createUser();

const stub = sandbox.stub(uw.chat, 'isMuted');
stub.withArgs(sinon.match({ id: user.id })).resolves(false);
stub.withArgs(sinon.match({ id: mutedUser.id })).resolves(true);
await supertest(uw.server)
.post(`/api/users/${mutedUser.id}/mute`)
.set('Cookie', `uwsession=${token}`)
.send({ time: 60 /* seconds */ })
.expect(200);

const ws = await uw.test.connectToWebSocketAs(user);
const mutedWs = await uw.test.connectToWebSocketAs(mutedUser);
Expand All @@ -51,9 +58,173 @@ describe('Chat', () => {
ws.send(JSON.stringify({ command: 'sendChat', data: 'unmuted' }));
mutedWs.send(JSON.stringify({ command: 'sendChat', data: 'muted' }));

await delay(1500);
await retryFor(1500, () => {
assert(receivedMessages.some((message) => message.command === 'chatMessage' && message.data.userID === user.id));
assert(!receivedMessages.some((message) => message.command === 'chatMessage' && message.data.userID === mutedUser.id));
});
});

describe('DELETE /chat/', () => {
it('requires authentication', async () => {
await supertest(uw.server)
.delete('/api/chat')
.expect(401);
});

it('requires the chat.delete permission', async () => {
const user = await uw.test.createUser();
const token = await uw.test.createTestSessionToken(user);

await supertest(uw.server)
.delete('/api/chat')
.set('Cookie', `uwsession=${token}`)
.expect(403);

await uw.acl.createRole('chatDeleter', ['chat.delete']);
await uw.acl.allow(user, ['chatDeleter']);

await supertest(uw.server)
.delete('/api/chat')
.set('Cookie', `uwsession=${token}`)
.expect(200);
});

it('broadcasts delete messages', async () => {
const user = await uw.test.createUser();
const token = await uw.test.createTestSessionToken(user);
await uw.acl.createRole('chatDeleter', ['chat.delete']);
await uw.acl.allow(user, ['chatDeleter']);

const otherUser = await uw.test.createUser();
const ws = await uw.test.connectToWebSocketAs(otherUser);

const receivedMessages = [];
ws.on('message', (data) => {
receivedMessages.push(JSON.parse(data));
});

await supertest(uw.server)
.delete('/api/chat')
.set('Cookie', `uwsession=${token}`)
.expect(200);

await retryFor(1500, () => {
sinon.assert.match(receivedMessages, sinon.match.some(sinon.match.has('command', 'chatDelete')));
});
});
});

describe('DELETE /chat/user/:id', () => {
it('requires authentication', async () => {
const user = await uw.test.createUser();

await supertest(uw.server)
.delete(`/api/chat/user/${user.id}`)
.expect(401);
});

it('requires the chat.delete permission', async () => {
const user = await uw.test.createUser();
const token = await uw.test.createTestSessionToken(user);

await supertest(uw.server)
.delete(`/api/chat/user/${user.id}`)
.set('Cookie', `uwsession=${token}`)
.expect(403);

await uw.acl.createRole('chatDeleter', ['chat.delete']);
await uw.acl.allow(user, ['chatDeleter']);

assert(receivedMessages.some((message) => message.command === 'chatMessage' && message.data.userID === user.id));
assert(!receivedMessages.some((message) => message.command === 'chatMessage' && message.data.userID === mutedUser.id));
await supertest(uw.server)
.delete(`/api/chat/user/${user.id}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);
});

it('broadcasts delete messages', async () => {
const user = await uw.test.createUser();
const token = await uw.test.createTestSessionToken(user);
await uw.acl.createRole('chatDeleter', ['chat.delete']);
await uw.acl.allow(user, ['chatDeleter']);

const otherUser = await uw.test.createUser();
const ws = await uw.test.connectToWebSocketAs(otherUser);

const receivedMessages = [];
ws.on('message', (data) => {
receivedMessages.push(JSON.parse(data));
});

await supertest(uw.server)
.delete(`/api/chat/user/${otherUser.id}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);

await retryFor(1500, () => {
sinon.assert.match(receivedMessages, sinon.match.some(sinon.match({
command: 'chatDeleteByUser',
data: sinon.match({
userID: otherUser.id,
}),
})));
});
});
});

describe('DELETE /chat/:id', () => {
const messageID = randomUUID();

it('requires authentication', async () => {
await supertest(uw.server)
.delete(`/api/chat/${messageID}`)
.expect(401);
});

it('requires the chat.delete permission', async () => {
const user = await uw.test.createUser();
const token = await uw.test.createTestSessionToken(user);

await supertest(uw.server)
.delete(`/api/chat/${messageID}`)
.set('Cookie', `uwsession=${token}`)
.expect(403);

await uw.acl.createRole('chatDeleter', ['chat.delete']);
await uw.acl.allow(user, ['chatDeleter']);

await supertest(uw.server)
.delete(`/api/chat/${messageID}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);
});

it('broadcasts delete messages', async () => {
const user = await uw.test.createUser();
const token = await uw.test.createTestSessionToken(user);
await uw.acl.createRole('chatDeleter', ['chat.delete']);
await uw.acl.allow(user, ['chatDeleter']);

const otherUser = await uw.test.createUser();
const ws = await uw.test.connectToWebSocketAs(otherUser);

const receivedMessages = [];
ws.on('message', (data) => {
receivedMessages.push(JSON.parse(data));
});

await supertest(uw.server)
.delete(`/api/chat/${messageID}`)
.set('Cookie', `uwsession=${token}`)
.expect(200);

await retryFor(1500, () => {
sinon.assert.match(receivedMessages, sinon.match.some(sinon.match({
command: 'chatDeleteByID',
data: sinon.match({
_id: messageID,
}),
})));
});
});
});
});
20 changes: 20 additions & 0 deletions test/utils/retry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import delay from 'delay';

/** Retry the `fn` until it doesn't throw, or until the duration in milliseconds has elapsed. */
export async function retryFor(duration, fn) {
const end = Date.now() + duration;
let caughtError;
while (Date.now() < end) {
try {
const result = await fn();
return result;
} catch (err) {
caughtError = err;
}
await delay(10);
}

if (caughtError != null) {
throw new Error(`Failed after ${duration}ms`, { cause: caughtError });
}
}

0 comments on commit 4ec71f3

Please sign in to comment.