import { Server } from 'node:http'
import pino from 'pino'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createApp, type WebContext } from '../src/app.ts'
import { migrateToLatest } from '../src/db/migrate.ts'
import { SettingsStore } from '../src/settings/store.ts'
import { type DevWeb, loadDevWeb } from '../src/web.ts'
import { authDeps } from './helpers/auth.ts'
import { testConfig } from './helpers/config.ts'
import { createSqliteDb } from './helpers/db.ts'
const logger = pino({ level: 'silent' })
/** A signed-in user, as /api/me answers for them. */
const me = (changes: Record = {}) => ({
did: 'did:plc:alice',
handle: 'alice.test',
storageMode: 'local',
backgroundSync: true,
roles: ['user'],
admin: false,
access: 'full',
accessMessage: null,
...changes,
})
/**
* The web app rendered on the server through Vite's SSR runner, as `pnpm dev` serves it, so each
* request runs the routes' server-side `beforeLoad`.
*/
describe('server-rendered pages', () => {
let dev: DevWeb
beforeAll(async () => {
dev = await loadDevWeb(new Server(), { reactScan: false })
}, 120_000)
afterAll(() => dev?.close())
/**
* Request a page as Hono hands it to Start, with /api/me answering for the given user, or
* signed out without one.
*/
function requestAs(user: ReturnType | null, path: string) {
const fetch: WebContext['fetch'] = async (input) => {
if (String(input) !== '/api/me') return Response.json({})
return user ? Response.json(user) : Response.json({ error: 'Unauthorized' }, { status: 401 })
}
return dev.web.fetch(new Request(`http://localhost${path}`), {
appName: 'Test Chat',
description: 'Chats on your PDS.',
publicUrl: 'https://chat.example',
fetch,
})
}
describe('through the server', () => {
const db = createSqliteDb()
const settings = new SettingsStore(db, logger, {
general: { appName: 'Test Chat', description: 'Chats on your PDS.' },
})
beforeAll(() => migrateToLatest(db))
afterAll(() => db.destroy())
const request = (path: string) =>
createApp({
config: testConfig(),
db,
logger,
settings,
auth: authDeps(db),
web: dev.web,
}).request(path)
it.each(['/', '/chat/abc', '/settings', '/admin/users'])(
'redirects a signed-out request for %s to /login before rendering',
async (path) => {
const res = await request(path)
expect(res.status).toBe(307)
expect(res.headers.get('location')).toBe('/login')
expect(await res.text()).toBe('')
},
)
it('titles the page with the configured app name', async () => {
const res = await request('/login')
expect(res.status).toBe(200)
const html = await res.text()
expect(html).toContain('Test Chat')
expect(html).toContain('')
})
})
it('lets search engines list the login page, under the public URL, with the description', async () => {
const html = await (await requestAs(null, '/login')).text()
expect(html).toContain('')
expect(html).toContain('')
expect(html).toContain('')
expect(html).not.toContain('content="noindex"')
expect(html).toContain('')
expect(html).toContain('Chats on your PDS.
')
})
it("keeps someone's own pages out of search engines, titled by page", async () => {
const html = await (await requestAs(me(), '/settings')).text()
expect(html).toContain('')
expect(html).toContain('Preferences · Settings · Test Chat')
expect(html).toContain(
'',
)
})
it.each(['/chat/abc', '/settings'])(
'redirects a viewer requesting %s to /login before rendering',
async (path) => {
const res = await requestAs(me({ access: 'viewer' }), path)
expect(res.status).toBe(307)
expect(res.headers.get('location')).toBe('/login')
expect(await res.text()).toBe('')
},
)
it.each(['/admin', '/admin/roles', '/admin/models'])(
'redirects a user who is not an admin requesting %s to / before rendering',
async (path) => {
const res = await requestAs(me(), path)
expect(res.status).toBe(307)
expect(res.headers.get('location')).toBe('/')
expect(await res.text()).toBe('')
},
)
it('renders the admin area for an admin', async () => {
const res = await requestAs(me({ admin: true }), '/admin/roles')
expect(res.status).toBe(200)
})
})