import {test, expect} from '../../support/fixtures' import {openWs, waitForMessage, sendMessage, authenticateWs, subscribeWs, collectMessages, closeWs} from '../../support/websocket' import {UserFactory} from '../../factories/user' import {TEST_PASSWORD} from '../../support/constants' import type {APIRequestContext} from '@playwright/test' /** Login without setting page localStorage — just returns the token. */ async function loginRaw(apiContext: APIRequestContext, user: {username: string}): Promise<{token: string}> { const response = await apiContext.post('login', { data: {username: user.username, password: TEST_PASSWORD}, }) return response.json() } test.describe('WebSocket Protocol', () => { test.describe('Authentication', () => { test('authenticates with valid token', async ({userToken}) => { const ws = await openWs() try { const msg = await authenticateWs(ws, userToken) expect(msg.action).toBe('auth.success') expect(msg.success).toBe(true) } finally { closeWs(ws) } }) test('rejects invalid token', async () => { const ws = await openWs() try { sendMessage(ws, {action: 'auth', token: 'invalid-token'}) const msg = await waitForMessage(ws) expect(msg.error).toBe('invalid_token') } finally { closeWs(ws) } }) test('closes connection after auth timeout', async () => { test.setTimeout(45000) const ws = await openWs() const closed = new Promise<{code: number; reason: string}>((resolve) => { ws.on('close', (code, reason) => { resolve({code, reason: reason.toString()}) }) }) const result = await closed // websocket StatusPolicyViolation = 1008 expect(result.code).toBe(1008) }) test('rejects double authentication', async ({userToken}) => { const ws = await openWs() try { await authenticateWs(ws, userToken) sendMessage(ws, {action: 'auth', token: userToken}) const msg = await waitForMessage(ws) expect(msg.error).toBe('already_authenticated') } finally { closeWs(ws) } }) }) test.describe('Subscribe / Unsubscribe Events', () => { test('subscribes to valid event', async ({userToken}) => { const ws = await openWs() try { await authenticateWs(ws, userToken) sendMessage(ws, {action: 'subscribe', event: 'notification.created'}) // No error response means success — verify by collecting messages // for a short window. If there was an error, it would arrive. const messages = await collectMessages(ws, 500) const errors = messages.filter(m => m.error) expect(errors).toHaveLength(0) } finally { closeWs(ws) } }) test('rejects invalid event', async ({userToken}) => { const ws = await openWs() try { await authenticateWs(ws, userToken) sendMessage(ws, {action: 'subscribe', event: 'nonexistent.event'}) const msg = await waitForMessage(ws) expect(msg.error).toBe('invalid_event') expect(msg.event).toBe('nonexistent.event') } finally { closeWs(ws) } }) test('requires auth before subscribe', async () => { const ws = await openWs() try { sendMessage(ws, {action: 'subscribe', event: 'notification.created'}) const msg = await waitForMessage(ws) expect(msg.error).toBe('auth_required') } finally { closeWs(ws) } }) test('unsubscribe stops receiving events', async ({apiContext, userToken, currentUser}) => { const ws = await openWs() try { await authenticateWs(ws, userToken) subscribeWs(ws, 'notification.created') // Create a second user to trigger the notification const [userA] = await UserFactory.create(1, {id: 100}, false) const {token: tokenA} = await loginRaw(apiContext, userA) // User A creates a team const teamResponse = await apiContext.put('teams', { data: {name: 'Test Team'}, headers: {Authorization: `Bearer ${tokenA}`}, }) const team = await teamResponse.json() // Unsubscribe before the notification is triggered sendMessage(ws, {action: 'unsubscribe', event: 'notification.created'}) // Give the server a moment to process the unsubscribe await new Promise(r => setTimeout(r, 200)) // Now add currentUser to team — should NOT receive WS notification await apiContext.put(`teams/${team.id}/members`, { data: {username: currentUser.username}, headers: {Authorization: `Bearer ${tokenA}`}, }) // Collect messages for 2 seconds — should get none const messages = await collectMessages(ws, 2000) const notifications = messages.filter(m => m.event === 'notification.created') expect(notifications).toHaveLength(0) } finally { closeWs(ws) } }) }) test.describe('Message Delivery', () => { test('receives notification when added to team', async ({apiContext, userToken, currentUser}) => { const ws = await openWs() try { await authenticateWs(ws, userToken) subscribeWs(ws, 'notification.created') // Create a second user (the doer) const [userA] = await UserFactory.create(1, {id: 100}, false) const {token: tokenA} = await loginRaw(apiContext, userA) // User A creates a team const teamResponse = await apiContext.put('teams', { data: {name: 'Notification Test Team'}, headers: {Authorization: `Bearer ${tokenA}`}, }) const team = await teamResponse.json() // User A adds currentUser to the team const addResponse = await apiContext.put(`teams/${team.id}/members`, { data: {username: currentUser.username}, headers: {Authorization: `Bearer ${tokenA}`}, }) expect(addResponse.ok()).toBe(true) // currentUser should receive the notification via WebSocket const msg = await waitForMessage(ws, 10000) expect(msg.event).toBe('notification.created') expect(msg.data).toBeDefined() } finally { closeWs(ws) } }) test('doer does not receive own notification', async ({apiContext, userToken}) => { const ws = await openWs() try { await authenticateWs(ws, userToken) subscribeWs(ws, 'notification.created') // Create a second user const [otherUser] = await UserFactory.create(1, {id: 100}, false) // currentUser creates a team (they are the doer) const teamResponse = await apiContext.put('teams', { data: {name: 'Doer Test Team'}, headers: {Authorization: `Bearer ${userToken}`}, }) const team = await teamResponse.json() // currentUser adds otherUser — currentUser is the doer await apiContext.put(`teams/${team.id}/members`, { data: {username: otherUser.username}, headers: {Authorization: `Bearer ${userToken}`}, }) // currentUser should NOT receive a notification (they did the action) const messages = await collectMessages(ws, 3000) const notifications = messages.filter(m => m.event === 'notification.created') expect(notifications).toHaveLength(0) } finally { closeWs(ws) } }) test('multiple connections receive same notification', async ({apiContext, userToken, currentUser}) => { const ws1 = await openWs() const ws2 = await openWs() try { // Both connections authenticate as the same user await authenticateWs(ws1, userToken) await authenticateWs(ws2, userToken) subscribeWs(ws1, 'notification.created') subscribeWs(ws2, 'notification.created') // Create a second user to trigger notification const [userA] = await UserFactory.create(1, {id: 100}, false) const {token: tokenA} = await loginRaw(apiContext, userA) const teamResponse = await apiContext.put('teams', { data: {name: 'Multi-Connection Team'}, headers: {Authorization: `Bearer ${tokenA}`}, }) const team = await teamResponse.json() await apiContext.put(`teams/${team.id}/members`, { data: {username: currentUser.username}, headers: {Authorization: `Bearer ${tokenA}`}, }) // Both connections should receive the notification const [msg1, msg2] = await Promise.all([ waitForMessage(ws1, 10000), waitForMessage(ws2, 10000), ]) expect(msg1.event).toBe('notification.created') expect(msg2.event).toBe('notification.created') } finally { closeWs(ws1) closeWs(ws2) } }) }) })