feat: support concurrent Reddit accounts
This commit is contained in:
1 parent
d819a0f1e6
commit
16a31335d0
51 files changed
+3241
-250
No files matched your search
@@ -6,12 +6,16 @@ import { createConversationsRouter } from '../../src/web/routes/conversations.js
|
||||
|
||||
const servers: Array<ReturnType<express.Express['listen']>> = [];
|
||||
|
||||
async function request(app: express.Express, path: string): Promise<Response> {
|
||||
async function request(
|
||||
app: express.Express,
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> {
|
||||
const server = app.listen(0);
|
||||
servers.push(server);
|
||||
await new Promise<void>((resolve) => server.once('listening', resolve));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
return fetch(`http://127.0.0.1:${port}${path}`);
|
||||
return fetch(`http://127.0.0.1:${port}${path}`, init);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -19,6 +23,125 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe('conversations route', () => {
|
||||
it('aggregates simultaneous account runtimes and labels identical room IDs with their owner', async () => {
|
||||
const chat = (username: string) => ({
|
||||
username, roomName: username, roomId: 'same-room', lastMessageTime: 1,
|
||||
lastMessageSnippet: `from ${username}`, isFromUs: false, unread: false,
|
||||
});
|
||||
const runtime = (accountId: string, redditUsername: string, conversationUsername: string) => ({
|
||||
session: { accountId, redditUsername },
|
||||
resources: { chatService: { getCachedConversations: vi.fn().mockResolvedValue([chat(conversationUsername)]) } },
|
||||
});
|
||||
const runtimes = new Map([
|
||||
['@t2_one:reddit.com', runtime('@t2_one:reddit.com', 'owner-one', 'alice')],
|
||||
['@t2_two:reddit.com', runtime('@t2_two:reddit.com', 'owner-two', 'bob')],
|
||||
]);
|
||||
const context = {
|
||||
logger: { error: vi.fn() },
|
||||
services: {
|
||||
chat: { getCachedConversations: vi.fn() },
|
||||
accountRuntimes: {
|
||||
list: () => [...runtimes].map(([accountId, item]) => ({ accountId, redditUsername: item.session.redditUsername, state: 'running' })),
|
||||
get: (accountId: string) => runtimes.get(accountId) ?? null,
|
||||
},
|
||||
},
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createConversationsRouter(context));
|
||||
|
||||
const response = await request(app, '/api/conversations');
|
||||
const body = await response.json();
|
||||
|
||||
expect(body).toHaveLength(2);
|
||||
expect(body).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ roomId: 'same-room', username: 'alice', owner: { accountId: '@t2_one:reddit.com', redditUsername: 'owner-one' } }),
|
||||
expect.objectContaining({ roomId: 'same-room', username: 'bob', owner: { accountId: '@t2_two:reddit.com', redditUsername: 'owner-two' } }),
|
||||
]));
|
||||
});
|
||||
|
||||
it('sends an owned conversation reply only through its account runtime', async () => {
|
||||
const firstAccountId = '@t2_one:reddit.com';
|
||||
const secondAccountId = '@t2_two:reddit.com';
|
||||
const conversation = {
|
||||
username: 'alice', roomName: 'alice', roomId: 'same-room', url: '/room/same-room',
|
||||
lastMessageTime: 1, lastMessageSnippet: 'hello', isFromUs: false, unread: false,
|
||||
messages: [{ sender: 'alice', text: 'hello', timestamp: 1, isSelf: false }],
|
||||
};
|
||||
const runtime = (accountId: string, redditUsername: string) => ({
|
||||
session: { accountId, redditUsername },
|
||||
snapshot: () => ({ state: 'running' }),
|
||||
resources: {
|
||||
chatService: {
|
||||
getCachedConversations: vi.fn().mockResolvedValue([conversation]),
|
||||
sendChatMessage: vi.fn().mockResolvedValue(true),
|
||||
refreshTopChats: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
conversationStateRepo: { get: vi.fn(), save: vi.fn() },
|
||||
activityLogRepo: { insert: vi.fn() },
|
||||
},
|
||||
});
|
||||
const first = runtime(firstAccountId, 'owner-one');
|
||||
const second = runtime(secondAccountId, 'owner-two');
|
||||
const singletonSend = vi.fn();
|
||||
const runtimes = new Map([[firstAccountId, first], [secondAccountId, second]]);
|
||||
const context = {
|
||||
logger: { error: vi.fn() },
|
||||
activity: { push: vi.fn() },
|
||||
broadcaster: { push: vi.fn() },
|
||||
services: {
|
||||
chat: { sendChatMessage: singletonSend },
|
||||
accountRuntimes: {
|
||||
list: () => [...runtimes.values()].map((item) => ({
|
||||
accountId: item.session.accountId,
|
||||
redditUsername: item.session.redditUsername,
|
||||
state: 'running',
|
||||
})),
|
||||
get: (accountId: string) => runtimes.get(accountId) ?? null,
|
||||
},
|
||||
},
|
||||
} as unknown as WebContext;
|
||||
const app = express()
|
||||
.use(express.json())
|
||||
.use('/api', createConversationsRouter(context));
|
||||
const ownedId = `${encodeURIComponent(secondAccountId)}~${encodeURIComponent('same-room')}`;
|
||||
|
||||
const response = await request(app, `/api/conversations/${encodeURIComponent(ownedId)}/send-reply`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ message: 'reply from account two' }),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(second.resources.chatService.sendChatMessage).toHaveBeenCalledWith(
|
||||
'/room/same-room',
|
||||
'reply from account two',
|
||||
);
|
||||
expect(first.resources.chatService.sendChatMessage).not.toHaveBeenCalled();
|
||||
expect(singletonSend).not.toHaveBeenCalled();
|
||||
expect(second.resources.activityLogRepo.insert).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'sent', recipient: 'alice',
|
||||
}));
|
||||
expect(first.resources.activityLogRepo.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects an unowned conversation ID while account runtimes are active', async () => {
|
||||
const context = {
|
||||
logger: { error: vi.fn() },
|
||||
services: {
|
||||
chat: { getCachedConversations: vi.fn().mockResolvedValue([]) },
|
||||
accountRuntimes: {
|
||||
list: () => [{ accountId: '@t2_one:reddit.com', redditUsername: 'owner-one', state: 'running' }],
|
||||
get: vi.fn(),
|
||||
},
|
||||
},
|
||||
} as unknown as WebContext;
|
||||
const app = express().use('/api', createConversationsRouter(context));
|
||||
|
||||
const response = await request(app, '/api/conversations/same-room');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(context.services.chat.getCachedConversations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports an unavailable chat service instead of presenting it as an empty inbox', async () => {
|
||||
const context = {
|
||||
logger: { error: vi.fn() },
|
||||
|
||||
Reference in new issue
Block a user