Barazo AppView backend
barazo.forum
1import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2import Fastify from 'fastify'
3import type { FastifyInstance } from 'fastify'
4import { createRequireOperator } from '../../../src/auth/require-operator.js'
5import type { AuthMiddleware, RequestUser } from '../../../src/auth/middleware.js'
6import type { Env } from '../../../src/config/env.js'
7import type { Logger } from '../../../src/lib/logger.js'
8
9// ---------------------------------------------------------------------------
10// Mock auth middleware
11// ---------------------------------------------------------------------------
12
13function createMockAuthMiddleware(): AuthMiddleware {
14 return {
15 requireAuth: vi.fn(async (_request, _reply) => {
16 // Simulate setting user - tests will set request.user before calling
17 }),
18 optionalAuth: vi.fn(),
19 }
20}
21
22// ---------------------------------------------------------------------------
23// Mock logger
24// ---------------------------------------------------------------------------
25
26const logInfoFn = vi.fn()
27const logWarnFn = vi.fn()
28
29function createMockLogger(): Logger {
30 return {
31 info: logInfoFn,
32 error: vi.fn(),
33 warn: logWarnFn,
34 debug: vi.fn(),
35 fatal: vi.fn(),
36 trace: vi.fn(),
37 child: vi.fn(),
38 silent: vi.fn(),
39 level: 'silent',
40 } as unknown as Logger
41}
42
43// ---------------------------------------------------------------------------
44// Mock env
45// ---------------------------------------------------------------------------
46
47function createMockEnv(
48 overrides: Partial<Pick<Env, 'COMMUNITY_MODE' | 'OPERATOR_DIDS'>> = {}
49): Pick<Env, 'COMMUNITY_MODE' | 'OPERATOR_DIDS'> {
50 return {
51 COMMUNITY_MODE: overrides.COMMUNITY_MODE ?? 'multi',
52 OPERATOR_DIDS: overrides.OPERATOR_DIDS ?? ['did:plc:operator123'],
53 }
54}
55
56// ---------------------------------------------------------------------------
57// Fixtures
58// ---------------------------------------------------------------------------
59
60const OPERATOR_USER: RequestUser = {
61 did: 'did:plc:operator123',
62 handle: 'operator.bsky.social',
63 sid: 's'.repeat(64),
64}
65
66const NON_OPERATOR_USER: RequestUser = {
67 did: 'did:plc:user456',
68 handle: 'user.bsky.social',
69 sid: 's'.repeat(64),
70}
71
72// ---------------------------------------------------------------------------
73// Tests
74// ---------------------------------------------------------------------------
75
76describe('requireOperator middleware', () => {
77 let app: FastifyInstance
78 let mockAuthMiddleware: AuthMiddleware
79
80 afterEach(async () => {
81 await app.close()
82 })
83
84 beforeEach(() => {
85 vi.clearAllMocks()
86 })
87
88 // Helper to build a Fastify app with the operator middleware
89 async function buildApp(
90 envOverrides: Partial<Pick<Env, 'COMMUNITY_MODE' | 'OPERATOR_DIDS'>> = {},
91 withLogger = true
92 ): Promise<FastifyInstance> {
93 mockAuthMiddleware = createMockAuthMiddleware()
94 const mockEnv = createMockEnv(envOverrides)
95 const mockLogger = withLogger ? createMockLogger() : undefined
96
97 const requireOperator = createRequireOperator(mockEnv as Env, mockAuthMiddleware, mockLogger)
98
99 app = Fastify({ logger: false })
100 app.decorateRequest('user', undefined as RequestUser | undefined)
101
102 app.get('/operator-test', { preHandler: [requireOperator] }, (request) => {
103 return { user: request.user }
104 })
105
106 await app.ready()
107 return app
108 }
109
110 // -------------------------------------------------------------------------
111 // Community mode check
112 // -------------------------------------------------------------------------
113
114 it("returns 404 if COMMUNITY_MODE is 'single'", async () => {
115 await buildApp({ COMMUNITY_MODE: 'single' })
116
117 const response = await app.inject({
118 method: 'GET',
119 url: '/operator-test',
120 })
121
122 expect(response.statusCode).toBe(404)
123 expect(response.json<{ error: string }>()).toStrictEqual({
124 error: 'Not found',
125 })
126 // requireAuth should NOT have been called
127 expect(mockAuthMiddleware.requireAuth).not.toHaveBeenCalled()
128 })
129
130 // -------------------------------------------------------------------------
131 // Authentication check (delegated to requireAuth)
132 // -------------------------------------------------------------------------
133
134 it('returns 401 when requireAuth rejects (no token)', async () => {
135 await buildApp({ COMMUNITY_MODE: 'multi' })
136
137 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (_request, reply) => {
138 await reply.status(401).send({ error: 'Authentication required' })
139 })
140
141 const response = await app.inject({
142 method: 'GET',
143 url: '/operator-test',
144 })
145
146 expect(response.statusCode).toBe(401)
147 expect(response.json<{ error: string }>()).toStrictEqual({
148 error: 'Authentication required',
149 })
150 })
151
152 // -------------------------------------------------------------------------
153 // Operator DID check
154 // -------------------------------------------------------------------------
155
156 it('returns 403 if user DID is not in OPERATOR_DIDS', async () => {
157 await buildApp({
158 COMMUNITY_MODE: 'multi',
159 OPERATOR_DIDS: ['did:plc:operator123'],
160 })
161
162 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
163 request.user = NON_OPERATOR_USER
164 })
165
166 const response = await app.inject({
167 method: 'GET',
168 url: '/operator-test',
169 })
170
171 expect(response.statusCode).toBe(403)
172 expect(response.json<{ error: string }>()).toStrictEqual({
173 error: 'Operator access required',
174 })
175 })
176
177 it('returns 403 when requireAuth passes but request.user is not set', async () => {
178 await buildApp({ COMMUNITY_MODE: 'multi' })
179
180 // requireAuth passes without setting request.user
181 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (_request, _reply) => {
182 // intentionally do not set request.user
183 })
184
185 const response = await app.inject({
186 method: 'GET',
187 url: '/operator-test',
188 })
189
190 expect(response.statusCode).toBe(403)
191 expect(response.json<{ error: string }>()).toStrictEqual({
192 error: 'Operator access required',
193 })
194 })
195
196 // -------------------------------------------------------------------------
197 // Success path
198 // -------------------------------------------------------------------------
199
200 it("grants access if user DID is in OPERATOR_DIDS and mode is 'multi'", async () => {
201 await buildApp({
202 COMMUNITY_MODE: 'multi',
203 OPERATOR_DIDS: ['did:plc:operator123'],
204 })
205
206 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
207 request.user = OPERATOR_USER
208 })
209
210 const response = await app.inject({
211 method: 'GET',
212 url: '/operator-test',
213 })
214
215 expect(response.statusCode).toBe(200)
216 const body = response.json<{ user: RequestUser }>()
217 expect(body.user).toStrictEqual(OPERATOR_USER)
218 })
219
220 it('grants access when OPERATOR_DIDS contains multiple DIDs', async () => {
221 await buildApp({
222 COMMUNITY_MODE: 'multi',
223 OPERATOR_DIDS: ['did:plc:other999', 'did:plc:operator123', 'did:plc:another888'],
224 })
225
226 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
227 request.user = OPERATOR_USER
228 })
229
230 const response = await app.inject({
231 method: 'GET',
232 url: '/operator-test',
233 })
234
235 expect(response.statusCode).toBe(200)
236 const body = response.json<{ user: RequestUser }>()
237 expect(body.user).toStrictEqual(OPERATOR_USER)
238 })
239
240 // -------------------------------------------------------------------------
241 // Audit logging
242 // -------------------------------------------------------------------------
243
244 it('logs audit trail when operator access is denied (DID not in list)', async () => {
245 await buildApp({
246 COMMUNITY_MODE: 'multi',
247 OPERATOR_DIDS: ['did:plc:operator123'],
248 })
249
250 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
251 request.user = NON_OPERATOR_USER
252 })
253
254 await app.inject({
255 method: 'GET',
256 url: '/operator-test',
257 })
258
259 expect(logWarnFn).toHaveBeenCalledWith(
260 { did: NON_OPERATOR_USER.did, url: '/operator-test', method: 'GET' },
261 'Operator access denied: DID not in OPERATOR_DIDS'
262 )
263 })
264
265 it('logs audit trail when operator access is denied (no user after auth)', async () => {
266 await buildApp({ COMMUNITY_MODE: 'multi' })
267
268 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (_request, _reply) => {
269 // intentionally do not set request.user
270 })
271
272 await app.inject({
273 method: 'GET',
274 url: '/operator-test',
275 })
276
277 expect(logWarnFn).toHaveBeenCalledWith(
278 { url: '/operator-test', method: 'GET' },
279 'Operator access denied: no user after auth'
280 )
281 })
282
283 it('logs audit trail when operator access is granted', async () => {
284 await buildApp({
285 COMMUNITY_MODE: 'multi',
286 OPERATOR_DIDS: ['did:plc:operator123'],
287 })
288
289 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
290 request.user = OPERATOR_USER
291 })
292
293 await app.inject({
294 method: 'GET',
295 url: '/operator-test',
296 })
297
298 expect(logInfoFn).toHaveBeenCalledWith(
299 { did: OPERATOR_USER.did, url: '/operator-test', method: 'GET' },
300 'Operator access granted'
301 )
302 })
303})