| import assert from 'node:assert/strict' |
| import { mkdtempSync, rmSync } from 'node:fs' |
| import { tmpdir } from 'node:os' |
| import path from 'node:path' |
| import test, { after } from 'node:test' |
|
|
| const testDataDir = mkdtempSync(path.join(tmpdir(), 'music-together-offline-members-')) |
| process.env.DATABASE_URL = `file:${path.join(testDataDir, 'test.db')}` |
|
|
| const roomService = await import('../src/services/roomService.js') |
| const { db } = await import('../src/repositories/database.js') |
| const { InMemoryRoomRepository, roomRepo } = await import('../src/repositories/roomRepository.js') |
|
|
| after(() => { |
| db.close() |
| rmSync(testDataDir, { recursive: true, force: true }) |
| }) |
|
|
| test('keeps offline members in a permanent room roster after reload', () => { |
| const { room } = roomService.createRoom('owner-socket', 'Owner', 'Roster room', null, 'owner-id') |
| roomService.updateSettings(room.id, { permanent: true }) |
|
|
| const joined = roomService.joinRoom('member-socket', room.id, 'Member', 'member-id') |
| assert.ok(joined) |
| assert.equal(joined.room.members.length, 2) |
| assert.equal(joined.room.members.find((member) => member.id === 'member-id')?.isOnline, true) |
|
|
| roomService.leaveRoom('member-socket') |
| const offlineMember = room.members.find((member) => member.id === 'member-id') |
| assert.deepEqual( |
| offlineMember && { |
| id: offlineMember.id, |
| role: offlineMember.role, |
| isOnline: offlineMember.isOnline, |
| hasLastSeenAt: offlineMember.lastSeenAt !== null, |
| }, |
| { id: 'member-id', role: 'member', isOnline: false, hasLastSeenAt: true }, |
| ) |
|
|
| const persistedCount = db |
| .prepare<[string], { count: number }>('SELECT COUNT(*) AS count FROM permanent_room_members WHERE room_id = ?') |
| .get(room.id) |
| assert.equal(persistedCount?.count, 2) |
|
|
| const restoredRepo = new InMemoryRoomRepository() |
| const restoredRoom = restoredRepo.get(room.id) |
| assert.equal(restoredRoom?.users.length, 0) |
| assert.deepEqual( |
| restoredRoom?.members.map((member) => ({ id: member.id, isOnline: member.isOnline })), |
| [ |
| { id: 'owner-id', isOnline: false }, |
| { id: 'member-id', isOnline: false }, |
| ], |
| ) |
|
|
| roomRepo.delete(room.id) |
| }) |
|
|
| test('restores the permanent room track and saved playback position after reload', () => { |
| const { room } = roomService.createRoom('player-socket', 'Player', 'Playback room', null, 'player-id') |
| roomService.updateSettings(room.id, { permanent: true }) |
| room.currentTrack = { |
| id: 'saved-track', |
| title: 'Saved song', |
| artist: ['Saved artist'], |
| album: 'Saved album', |
| duration: 240, |
| cover: '', |
| source: 'netease', |
| sourceId: '123', |
| urlId: '123', |
| streamUrl: 'https://expired.example.test/stream', |
| } |
| room.playState = { isPlaying: true, currentTime: 91.25, serverTimestamp: Date.now() - 1_000 } |
| roomRepo.persist(room.id) |
|
|
| const restoredRoom = new InMemoryRoomRepository().get(room.id) |
| const { streamUrl: _streamUrl, ...expectedTrack } = room.currentTrack |
| assert.deepEqual(restoredRoom?.currentTrack, expectedTrack) |
| assert.deepEqual(restoredRoom?.playState.isPlaying, false) |
| assert.equal(restoredRoom?.playState.currentTime, 91.25) |
|
|
| roomRepo.delete(room.id) |
| }) |
|
|
| test('updates the roster role when an online member becomes temporary admin', () => { |
| const { room } = roomService.createRoom('owner-socket', 'Owner', 'Role sync room', null, 'owner-role-id') |
| const joined = roomService.joinRoom('member-socket', room.id, 'Member', 'member-role-id') |
| assert.ok(joined) |
|
|
| const left = roomService.leaveRoom('owner-socket') |
| assert.ok(left) |
| assert.equal(left.roleChanged, true) |
| assert.equal(room.users.find((user) => user.id === 'member-role-id')?.role, 'admin') |
| assert.equal(room.members.find((member) => member.id === 'member-role-id')?.role, 'admin') |
|
|
| roomRepo.delete(room.id) |
| }) |
|
|
| test('clears a temporary admin role when the room becomes empty before owner returns', () => { |
| const { room } = roomService.createRoom('owner-empty-socket', 'Owner', 'Role reset room', null, 'owner-empty-id') |
| const ownerLeft = roomService.leaveRoom('owner-empty-socket') |
| assert.ok(ownerLeft) |
|
|
| const memberJoined = roomService.joinRoom('member-empty-socket', room.id, 'Member', 'member-empty-id') |
| assert.ok(memberJoined) |
| assert.equal(room.members.find((member) => member.id === 'member-empty-id')?.role, 'admin') |
|
|
| const memberLeft = roomService.leaveRoom('member-empty-socket') |
| assert.ok(memberLeft) |
| assert.equal(room.members.find((member) => member.id === 'member-empty-id')?.role, 'member') |
|
|
| const ownerReturned = roomService.joinRoom('owner-return-socket', room.id, 'Owner', 'owner-empty-id') |
| assert.ok(ownerReturned) |
| assert.equal(ownerReturned.room.members.find((member) => member.id === 'member-empty-id')?.role, 'member') |
|
|
| roomRepo.delete(room.id) |
| }) |
|
|