Barazo AppView backend
barazo.forum
1import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2import Fastify from 'fastify'
3import type { FastifyInstance } from 'fastify'
4import { createRequireAdmin } from '../../../src/auth/require-admin.js'
5import type { AuthMiddleware, RequestUser } from '../../../src/auth/middleware.js'
6
7// ---------------------------------------------------------------------------
8// Mock database
9// ---------------------------------------------------------------------------
10
11interface MockUserRow {
12 did: string
13 handle: string
14 role: string
15}
16
17const mockDbSelect = vi.fn()
18const mockDbFrom = vi.fn()
19const mockDbWhere = vi.fn()
20
21function createMockDb() {
22 // Chain: db.select().from(users).where(eq(users.did, did))
23 mockDbWhere.mockReturnValue([])
24 mockDbFrom.mockReturnValue({ where: mockDbWhere })
25 mockDbSelect.mockReturnValue({ from: mockDbFrom })
26
27 return {
28 select: mockDbSelect,
29 }
30}
31
32// ---------------------------------------------------------------------------
33// Mock auth middleware
34// ---------------------------------------------------------------------------
35
36function createMockAuthMiddleware(): AuthMiddleware {
37 return {
38 requireAuth: vi.fn(async (_request, _reply) => {
39 // Simulate setting user - tests will set request.user before calling
40 }),
41 optionalAuth: vi.fn(),
42 }
43}
44
45// ---------------------------------------------------------------------------
46// Fixtures
47// ---------------------------------------------------------------------------
48
49const ADMIN_USER: RequestUser = {
50 did: 'did:plc:admin123',
51 handle: 'admin.bsky.social',
52 sid: 's'.repeat(64),
53}
54
55const REGULAR_USER: RequestUser = {
56 did: 'did:plc:user456',
57 handle: 'user.bsky.social',
58 sid: 's'.repeat(64),
59}
60
61const ADMIN_DB_ROW: MockUserRow = {
62 did: ADMIN_USER.did,
63 handle: ADMIN_USER.handle,
64 role: 'admin',
65}
66
67const REGULAR_DB_ROW: MockUserRow = {
68 did: REGULAR_USER.did,
69 handle: REGULAR_USER.handle,
70 role: 'user',
71}
72
73const MODERATOR_DB_ROW: MockUserRow = {
74 did: 'did:plc:mod789',
75 handle: 'mod.bsky.social',
76 role: 'moderator',
77}
78
79// ---------------------------------------------------------------------------
80// Tests
81// ---------------------------------------------------------------------------
82
83describe('requireAdmin middleware', () => {
84 let app: FastifyInstance
85 let mockAuthMiddleware: AuthMiddleware
86
87 beforeEach(async () => {
88 vi.clearAllMocks()
89
90 const mockDb = createMockDb()
91 mockAuthMiddleware = createMockAuthMiddleware()
92
93 const requireAdmin = createRequireAdmin(mockDb as never, mockAuthMiddleware)
94
95 app = Fastify({ logger: false })
96 app.decorateRequest('user', undefined as RequestUser | undefined)
97
98 app.get('/admin-test', { preHandler: [requireAdmin] }, (request) => {
99 return { user: request.user }
100 })
101
102 await app.ready()
103 })
104
105 afterEach(async () => {
106 await app.close()
107 })
108
109 it('returns 401 when requireAuth rejects (no token)', async () => {
110 // Make requireAuth return 401
111 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (_request, reply) => {
112 await reply.status(401).send({ error: 'Authentication required' })
113 })
114
115 const response = await app.inject({
116 method: 'GET',
117 url: '/admin-test',
118 })
119
120 expect(response.statusCode).toBe(401)
121 expect(response.json<{ error: string }>()).toStrictEqual({
122 error: 'Authentication required',
123 })
124 })
125
126 it('returns 403 when user is not found in database', async () => {
127 // requireAuth passes and sets user
128 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
129 request.user = ADMIN_USER
130 })
131
132 // User not found in DB
133 mockDbWhere.mockResolvedValueOnce([])
134
135 const response = await app.inject({
136 method: 'GET',
137 url: '/admin-test',
138 })
139
140 expect(response.statusCode).toBe(403)
141 expect(response.json<{ error: string }>()).toStrictEqual({
142 error: 'Admin access required',
143 })
144 })
145
146 it("returns 403 when user has role 'user'", async () => {
147 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
148 request.user = REGULAR_USER
149 })
150
151 mockDbWhere.mockResolvedValueOnce([REGULAR_DB_ROW])
152
153 const response = await app.inject({
154 method: 'GET',
155 url: '/admin-test',
156 })
157
158 expect(response.statusCode).toBe(403)
159 expect(response.json<{ error: string }>()).toStrictEqual({
160 error: 'Admin access required',
161 })
162 })
163
164 it("returns 403 when user has role 'moderator'", async () => {
165 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
166 request.user = {
167 did: MODERATOR_DB_ROW.did,
168 handle: MODERATOR_DB_ROW.handle,
169 sid: 's'.repeat(64),
170 }
171 })
172
173 mockDbWhere.mockResolvedValueOnce([MODERATOR_DB_ROW])
174
175 const response = await app.inject({
176 method: 'GET',
177 url: '/admin-test',
178 })
179
180 expect(response.statusCode).toBe(403)
181 expect(response.json<{ error: string }>()).toStrictEqual({
182 error: 'Admin access required',
183 })
184 })
185
186 it('passes through for admin user and returns 200', async () => {
187 vi.mocked(mockAuthMiddleware.requireAuth).mockImplementation(async (request, _reply) => {
188 request.user = ADMIN_USER
189 })
190
191 mockDbWhere.mockResolvedValueOnce([ADMIN_DB_ROW])
192
193 const response = await app.inject({
194 method: 'GET',
195 url: '/admin-test',
196 })
197
198 expect(response.statusCode).toBe(200)
199 const body = response.json<{ user: RequestUser }>()
200 expect(body.user).toStrictEqual(ADMIN_USER)
201 })
202})