Files
aris/apps/aelis-backend/src/admin/http.test.ts
Kenneth a6be7b31e7 feat(session): query enabled sources before providers (#85)
UserSessionManager now queries the user_sources table for enabled
sources before calling any provider. Providers receive the per-user
JSON config directly instead of querying the DB themselves, removing
their db dependency and eliminating redundant round-trips.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-22 16:28:19 +00:00

196 lines
5.3 KiB
TypeScript

import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@aelis/core"
import { describe, expect, mock, test } from "bun:test"
import { Hono } from "hono"
import type { AdminMiddleware } from "../auth/admin-middleware.ts"
import type { AuthSession, AuthUser } from "../auth/session.ts"
import type { Database } from "../db/index.ts"
import type { FeedSourceProvider } from "../session/feed-source-provider.ts"
import { UserSessionManager } from "../session/user-session-manager.ts"
import { registerAdminHttpHandlers } from "./http.ts"
let mockEnabledSourceIds: string[] = []
mock.module("../sources/user-sources.ts", () => ({
sources: (_db: Database, _userId: string) => ({
async enabled() {
const now = new Date()
return mockEnabledSourceIds.map((sourceId) => ({
id: crypto.randomUUID(),
userId: _userId,
sourceId,
enabled: true,
config: {},
credentials: null,
createdAt: now,
updatedAt: now,
}))
},
async find(sourceId: string) {
const now = new Date()
return {
id: crypto.randomUUID(),
userId: _userId,
sourceId,
enabled: true,
config: {},
credentials: null,
createdAt: now,
updatedAt: now,
}
},
}),
}))
function createStubSource(id: string): FeedSource {
return {
id,
async listActions(): Promise<Record<string, ActionDefinition>> {
return {}
},
async executeAction(): Promise<unknown> {
return undefined
},
async fetchContext(): Promise<readonly ContextEntry[] | null> {
return null
},
async fetchItems(): Promise<FeedItem[]> {
return []
},
}
}
function createStubProvider(sourceId: string): FeedSourceProvider {
return {
sourceId,
async feedSourceForUser() {
return createStubSource(sourceId)
},
}
}
/** Passthrough admin middleware for testing (assumes admin). */
function passthroughAdminMiddleware(): AdminMiddleware {
const now = new Date()
return async (c, next) => {
c.set("user", {
id: "admin-1",
name: "Admin",
email: "admin@test.com",
emailVerified: true,
image: null,
createdAt: now,
updatedAt: now,
role: "admin",
banned: false,
banReason: null,
banExpires: null,
} as AuthUser)
c.set("session", { id: "sess-1" } as AuthSession)
await next()
}
}
const fakeDb = {} as Database
function createApp(providers: FeedSourceProvider[]) {
mockEnabledSourceIds = providers.map((p) => p.sourceId)
const sessionManager = new UserSessionManager({ db: fakeDb, providers })
const app = new Hono()
registerAdminHttpHandlers(app, {
sessionManager,
adminMiddleware: passthroughAdminMiddleware(),
db: fakeDb,
})
return { app, sessionManager }
}
const validWeatherConfig = {
credentials: {
privateKey: "pk-123",
keyId: "key-456",
teamId: "team-789",
serviceId: "svc-abc",
},
}
describe("PUT /api/admin/:sourceId/config", () => {
test("returns 404 for unknown provider", async () => {
const { app } = createApp([createStubProvider("aelis.location")])
const res = await app.request("/api/admin/aelis.nonexistent/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "value" }),
})
expect(res.status).toBe(404)
const body = (await res.json()) as { error: string }
expect(body.error).toContain("not found")
})
test("returns 404 for provider without runtime config support", async () => {
const { app } = createApp([createStubProvider("aelis.location")])
const res = await app.request("/api/admin/aelis.location/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "value" }),
})
expect(res.status).toBe(404)
const body = (await res.json()) as { error: string }
expect(body.error).toContain("not found")
})
test("returns 400 for invalid JSON body", async () => {
const { app } = createApp([createStubProvider("aelis.weather")])
const res = await app.request("/api/admin/aelis.weather/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: "not json",
})
expect(res.status).toBe(400)
const body = (await res.json()) as { error: string }
expect(body.error).toContain("Invalid JSON")
})
test("returns 400 when weather config fails validation", async () => {
const { app } = createApp([createStubProvider("aelis.weather")])
const res = await app.request("/api/admin/aelis.weather/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ credentials: { privateKey: 123 } }),
})
expect(res.status).toBe(400)
const body = (await res.json()) as { error: string }
expect(body.error).toBeDefined()
})
test("returns 204 and applies valid weather config", async () => {
const { app, sessionManager } = createApp([createStubProvider("aelis.weather")])
const originalProvider = sessionManager.getProvider("aelis.weather")
const res = await app.request("/api/admin/aelis.weather/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(validWeatherConfig),
})
expect(res.status).toBe(204)
// Provider was replaced with a new instance
const provider = sessionManager.getProvider("aelis.weather")
expect(provider).toBeDefined()
expect(provider!.sourceId).toBe("aelis.weather")
expect(provider).not.toBe(originalProvider)
})
})