diff --git a/apps/freya-backend/src/conversations/http.test.ts b/apps/freya-backend/src/conversations/http.test.ts new file mode 100644 index 0000000..678c8ba --- /dev/null +++ b/apps/freya-backend/src/conversations/http.test.ts @@ -0,0 +1,110 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test" +import { Hono } from "hono" + +import type { Database } from "../db/index.ts" +import type { ConversationRow } from "./storage.ts" + +import { mockAuthSessionMiddleware } from "../auth/session-middleware.ts" +import { registerConversationsHttpHandlers } from "./http.ts" + +const MockUserId = "k7Gx2mPqRvNwYs9TdLfA4bHcJeUo1iZn" + +const conversationRowsByUser = new Map() + +mock.module("./storage.ts", () => ({ + conversations: (_db: Database, userId: string) => ({ + async listConversations(): Promise { + return conversationRowsByUser.get(userId) ?? [] + }, + }), +})) + +const fakeDb = {} as Database + +function buildTestApp(userId?: string) { + const app = new Hono() + registerConversationsHttpHandlers(app, { + db: fakeDb, + authSessionMiddleware: mockAuthSessionMiddleware(userId), + }) + return app +} + +function createConversationRow( + id: string, + createdAt: string, + updatedAt: string, + userId = MockUserId, +): ConversationRow { + return { + id, + userId, + createdAt: new Date(createdAt), + updatedAt: new Date(updatedAt), + } +} + +describe("GET /api/conversations", () => { + beforeEach(() => { + conversationRowsByUser.clear() + }) + + test("returns 401 without auth", async () => { + const app = buildTestApp() + + const res = await app.request("/api/conversations") + + expect(res.status).toBe(401) + }) + + test("returns conversation summaries for the authenticated user", async () => { + conversationRowsByUser.set(MockUserId, [ + createConversationRow( + "conversation-newer", + "2026-06-16T10:00:00.000Z", + "2026-06-17T09:30:00.000Z", + ), + createConversationRow( + "conversation-older", + "2026-06-15T10:00:00.000Z", + "2026-06-16T09:30:00.000Z", + ), + ]) + const app = buildTestApp("user-1") + + const res = await app.request("/api/conversations") + + expect(res.status).toBe(200) + const body = (await res.json()) as { + conversations: Array<{ id: string; createdAt: string; updatedAt: string }> + } + expect(body).toEqual({ + conversations: [ + { + id: "conversation-newer", + createdAt: "2026-06-16T10:00:00.000Z", + updatedAt: "2026-06-17T09:30:00.000Z", + }, + { + id: "conversation-older", + createdAt: "2026-06-15T10:00:00.000Z", + updatedAt: "2026-06-16T09:30:00.000Z", + }, + ], + }) + }) + + test("returns an empty list when no conversations exist", async () => { + const app = buildTestApp("user-1") + + const res = await app.request("/api/conversations") + + expect(res.status).toBe(200) + const body = (await res.json()) as { + conversations: Array<{ id: string; createdAt: string; updatedAt: string }> + } + expect(body).toEqual({ + conversations: [], + }) + }) +}) diff --git a/apps/freya-backend/src/conversations/http.ts b/apps/freya-backend/src/conversations/http.ts new file mode 100644 index 0000000..e036307 --- /dev/null +++ b/apps/freya-backend/src/conversations/http.ts @@ -0,0 +1,44 @@ +import type { Context, Hono } from "hono" + +import { createMiddleware } from "hono/factory" + +import type { AuthSessionMiddleware } from "../auth/session-middleware.ts" +import type { Database } from "../db/index.ts" + +import { conversations } from "./storage.ts" + +type Env = { + Variables: { + db: Database + } +} + +interface ConversationsHttpHandlersDeps { + db: Database + authSessionMiddleware: AuthSessionMiddleware +} + +export function registerConversationsHttpHandlers( + app: Hono, + { db, authSessionMiddleware }: ConversationsHttpHandlersDeps, +) { + const inject = createMiddleware(async (c, next) => { + c.set("db", db) + await next() + }) + + app.get("/api/conversations", inject, authSessionMiddleware, handleListConversations) +} + +async function handleListConversations(c: Context) { + const user = c.get("user")! + const db = c.get("db") + + return c.json({ + conversations: (await conversations(db, user.id).listConversations()).map((row) => ({ + id: row.id, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + })), + }) +} diff --git a/apps/freya-backend/src/conversations/storage.ts b/apps/freya-backend/src/conversations/storage.ts index de4c4ef..5b0b831 100644 --- a/apps/freya-backend/src/conversations/storage.ts +++ b/apps/freya-backend/src/conversations/storage.ts @@ -101,6 +101,14 @@ export function conversations(db: Database, userId: string) { return insertConversation(db, userId) }, + async listConversations(): Promise { + return db + .select() + .from(conversationsTable) + .where(eq(conversationsTable.userId, userId)) + .orderBy(desc(conversationsTable.updatedAt), desc(conversationsTable.createdAt)) + }, + async getOrCreateConversation(): Promise { return db.transaction(async (tx) => { await requireUserForUpdate(tx, userId) diff --git a/apps/freya-backend/src/server.ts b/apps/freya-backend/src/server.ts index 3d43def..19538aa 100644 --- a/apps/freya-backend/src/server.ts +++ b/apps/freya-backend/src/server.ts @@ -9,6 +9,7 @@ import { registerAuthHandlers } from "./auth/http.ts" import { createAuth } from "./auth/index.ts" import { createRequireSession } from "./auth/session-middleware.ts" import { CalDavSourceProvider } from "./caldav/provider.ts" +import { registerConversationsHttpHandlers } from "./conversations/http.ts" import { createDatabase } from "./db/index.ts" import { registerFeedHttpHandlers } from "./engine/http.ts" import { createFeedEnhancer } from "./enhancement/enhance-feed.ts" @@ -108,6 +109,7 @@ function main() { registerAuthHandlers(app, auth) + registerConversationsHttpHandlers(app, { db, authSessionMiddleware }) registerFeedHttpHandlers(app, { sessionManager, authSessionMiddleware,