import express from 'express'; import type { AddressInfo } from 'node:net'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { MediaResponseSchema } from '../../packages/api-contracts/src/media.js'; import type { WebContext } from '../../src/web/context.js'; import { createMediaRouter } from '../../src/web/routes/media.js'; const servers: Array> = []; async function request(app: express.Express, path: string): Promise { const server = app.listen(0); servers.push(server); await new Promise((resolve) => server.once('listening', resolve)); const { port } = server.address() as AddressInfo; return fetch(`http://127.0.0.1:${port}${path}`); } afterEach(async () => { await Promise.all(servers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve())))); }); describe('media route username presentation', () => { it('lists persisted received media for accounts whose runtimes are stopped', async () => { const storedItem = (accountId: string, mediaId: string, sender: string) => ({ accountId, mediaId, chatUsername: 'alice', chatRoomId: `room-${accountId}`, sender, timestamp: 1, originalUrl: `mxc://${mediaId}`, fileName: `${mediaId}.jpg`, relativePath: `${mediaId}.jpg`, createdAt: 2, }); const accounts = [ { accountId: 't2_running', redditUsername: 'running-owner' }, { accountId: 't2_stopped', redditUsername: 'stopped-owner' }, ]; const listMedia = vi.fn((accountId: string) => accountId === 't2_running' ? [storedItem(accountId, 'running-received', 'alice')] : [ storedItem(accountId, 'stopped-received', 'bob'), storedItem(accountId, 'stopped-sent', '@t2_stopped:reddit.com'), ]); const context = { logger: { error: vi.fn() }, repos: { accountContent: { listMedia } }, services: { media: { getMediaItems: vi.fn().mockResolvedValue([]) }, chat: { getCachedConversations: vi.fn().mockResolvedValue([]), resolveSenderDisplay: vi.fn((identity: string) => identity), }, redditAccounts: { list: vi.fn().mockReturnValue(accounts), get: vi.fn((accountId: string) => accounts.find((account) => account.accountId === accountId) ?? null), }, accountRuntimes: { list: vi.fn().mockReturnValue([{ accountId: 't2_running', state: 'running' }]), get: vi.fn().mockReturnValue({ resources: { mediaService: { getMediaItems: vi.fn().mockResolvedValue([]) }, chatService: { getCachedConversations: vi.fn().mockResolvedValue([]) }, } }), }, }, } as unknown as WebContext; const app = express().use('/api', createMediaRouter(context)); const response = await request(app, '/api/media'); const body = await response.json(); expect(listMedia).toHaveBeenCalledTimes(2); expect(body).toEqual([ expect.objectContaining({ id: 'running-received', owner: accounts[0] }), expect.objectContaining({ id: 'stopped-received', owner: accounts[1] }), ]); }); it('lists received media while excluding items sent by the owning account', async () => { const mediaItem = (id: string, sender: string) => ({ accountId: 't2_owner', id, chatUsername: 'alice', chatRoomId: 'room-one', sender, timestamp: 1, originalUrl: `mxc://${id}`, fileName: `${id}.jpg`, relativePath: `${id}.jpg`, createdAt: 2, }); const context = { logger: { error: vi.fn() }, services: { media: { getMediaItems: vi.fn().mockResolvedValue([ mediaItem('received', 'alice'), mediaItem('sent-marker', 'Me'), mediaItem('sent-matrix-id', '@t2_owner:reddit.com'), mediaItem('sent-username', 'owner'), ]) }, chat: { getCachedConversations: vi.fn().mockResolvedValue([]), resolveSenderDisplay: vi.fn((identity: string) => identity), }, redditAccounts: { list: vi.fn().mockReturnValue([]), get: vi.fn().mockReturnValue({ accountId: 't2_owner', redditUsername: 'owner' }), }, }, } as unknown as WebContext; const app = express().use('/api', createMediaRouter(context)); const response = await request(app, '/api/media'); expect(response.status).toBe(200); expect(await response.json()).toEqual([ expect.objectContaining({ id: 'received', sender: 'alice' }), ]); }); it('upgrades historical t2 fields through the chat username map', async () => { const context = { logger: { error: vi.fn() }, services: { media: { getMediaItems: vi.fn().mockResolvedValue([{ id: 'one', chatUsername: 't2_known', chatRoomId: 'room-one', sender: 't2_known', timestamp: 1, originalUrl: 'mxc://one', fileName: 'one.jpg', relativePath: 'one.jpg', createdAt: 2, }]) }, chat: { getCachedConversations: vi.fn().mockResolvedValue([]), resolveSenderDisplay: vi.fn((identity: string) => identity === 't2_known' ? 'alice' : identity), }, }, } as unknown as WebContext; const app = express().use('/api', createMediaRouter(context)); const response = await request(app, '/api/media'); expect(response.status).toBe(200); expect(await response.json()).toEqual([expect.objectContaining({ chatUsername: 'alice', sender: 'alice' })]); }); it('uses a safe fallback when no username mapping exists', async () => { const context = { logger: { error: vi.fn() }, services: { media: { getMediaItems: vi.fn().mockResolvedValue([{ id: 'one', chatUsername: 't2_missing', chatRoomId: 'room-one', sender: '@t2_missing:reddit.com', timestamp: 1, originalUrl: 'mxc://one', fileName: 'one.jpg', relativePath: 'one.jpg', createdAt: 2, }]) }, chat: { getCachedConversations: vi.fn().mockResolvedValue([]), resolveSenderDisplay: vi.fn((identity: string) => identity), }, }, } as unknown as WebContext; const app = express().use('/api', createMediaRouter(context)); const response = await request(app, '/api/media'); const body = await response.json(); expect(JSON.stringify(body)).not.toContain('t2_missing'); expect(body[0]).toMatchObject({ chatUsername: null, sender: 'Unknown Reddit user' }); }); it('keeps the internal account ID out of the public media item', async () => { const context = { logger: { error: vi.fn() }, services: { media: { getMediaItems: vi.fn().mockResolvedValue([{ accountId: 't2_owner', id: 'one', chatUsername: 'alice', chatRoomId: 'room-one', sender: 'alice', timestamp: 1, originalUrl: 'mxc://one', fileName: 'one.jpg', relativePath: 'one.jpg', createdAt: 2, }]) }, chat: { getCachedConversations: vi.fn().mockResolvedValue([]), resolveSenderDisplay: vi.fn((identity: string) => identity), }, redditAccounts: { list: vi.fn().mockReturnValue([]), get: vi.fn().mockReturnValue({ accountId: 't2_owner', redditUsername: 'owner' }), }, }, } as unknown as WebContext; const app = express().use('/api', createMediaRouter(context)); const response = await request(app, '/api/media'); const body: unknown = await response.json(); expect(MediaResponseSchema.parse(body)).toEqual([ expect.objectContaining({ id: 'one', owner: { accountId: 't2_owner', redditUsername: 'owner' }, }), ]); expect(body).not.toEqual([expect.objectContaining({ accountId: expect.anything() })]); }); });