Barazo default frontend barazo.forum
at main 359 lines 9.5 kB view raw
1/** 2 * Tests for PluginProvider context. 3 */ 4 5import { describe, it, expect, vi, beforeEach } from 'vitest' 6import { render, waitFor, act } from '@testing-library/react' 7import { PluginProvider, PluginContext } from './plugin-context' 8import type { PluginContextValue } from './plugin-context' 9import { getPlugins } from '@/lib/api/client' 10import { useAuth } from '@/hooks/use-auth' 11import { useContext, useEffect, useRef } from 'react' 12import type { Plugin } from '@/lib/api/types' 13 14vi.mock('@/lib/api/client', () => ({ 15 getPlugins: vi.fn(), 16})) 17 18vi.mock('@/hooks/use-auth', () => ({ 19 useAuth: vi.fn(), 20})) 21 22// Mutable container to capture context without TS control-flow narrowing 23interface ContextRef { 24 current: PluginContextValue | null 25} 26 27function createContextRef(): ContextRef { 28 return { current: null } 29} 30 31// Test consumer component that renders plugin context values 32function PluginConsumer({ ctxRef }: { ctxRef: ContextRef }) { 33 const context = useContext(PluginContext) 34 const ref = useRef(ctxRef) 35 useEffect(() => { 36 ref.current.current = context 37 }) 38 return ( 39 <div> 40 <span data-testid="loading">{String(context?.isLoading)}</span> 41 <span data-testid="plugin-count">{context?.plugins.length ?? 0}</span> 42 </div> 43 ) 44} 45 46const mockPlugins: Plugin[] = [ 47 { 48 id: '1', 49 name: 'analytics', 50 displayName: 'Analytics', 51 version: '1.0.0', 52 description: 'Analytics plugin', 53 source: 'core', 54 enabled: true, 55 category: 'analytics', 56 dependencies: [], 57 dependents: [], 58 settingsSchema: {}, 59 settings: { trackPageViews: true, sampleRate: 0.5 }, 60 installedAt: '2026-01-01T00:00:00Z', 61 }, 62 { 63 id: '2', 64 name: 'spam-filter', 65 displayName: 'Spam Filter', 66 version: '2.1.0', 67 description: 'Spam filtering plugin', 68 source: 'official', 69 enabled: false, 70 category: 'moderation', 71 dependencies: [], 72 dependents: [], 73 settingsSchema: {}, 74 settings: { threshold: 0.8 }, 75 installedAt: '2026-01-15T00:00:00Z', 76 }, 77] 78 79function mockAuthUnauthenticated() { 80 vi.mocked(useAuth).mockReturnValue({ 81 user: null, 82 isAuthenticated: false, 83 isLoading: false, 84 crossPostScopesGranted: false, 85 getAccessToken: () => null, 86 login: vi.fn(), 87 logout: vi.fn(), 88 setSessionFromCallback: vi.fn(), 89 requestCrossPostAuth: vi.fn(), 90 authFetch: vi.fn(), 91 } as ReturnType<typeof useAuth>) 92} 93 94function mockAuthAuthenticated() { 95 vi.mocked(useAuth).mockReturnValue({ 96 user: { 97 did: 'did:plc:test', 98 handle: 'test.bsky.social', 99 displayName: 'Test User', 100 avatarUrl: null, 101 role: 'user', 102 }, 103 isAuthenticated: true, 104 isLoading: false, 105 crossPostScopesGranted: false, 106 getAccessToken: () => 'mock-token', 107 login: vi.fn(), 108 logout: vi.fn(), 109 setSessionFromCallback: vi.fn(), 110 requestCrossPostAuth: vi.fn(), 111 authFetch: vi.fn(), 112 } as ReturnType<typeof useAuth>) 113} 114 115beforeEach(() => { 116 vi.clearAllMocks() 117}) 118 119describe('PluginProvider', () => { 120 it('provides empty plugins array when not authenticated', async () => { 121 mockAuthUnauthenticated() 122 123 const ctxRef = createContextRef() 124 render( 125 <PluginProvider> 126 <PluginConsumer ctxRef={ctxRef} /> 127 </PluginProvider> 128 ) 129 130 await waitFor(() => { 131 expect(ctxRef.current?.isLoading).toBe(false) 132 }) 133 134 expect(ctxRef.current?.plugins).toEqual([]) 135 expect(getPlugins).not.toHaveBeenCalled() 136 }) 137 138 it('fetches plugins when authenticated', async () => { 139 mockAuthAuthenticated() 140 vi.mocked(getPlugins).mockResolvedValue({ plugins: mockPlugins }) 141 142 const ctxRef = createContextRef() 143 render( 144 <PluginProvider> 145 <PluginConsumer ctxRef={ctxRef} /> 146 </PluginProvider> 147 ) 148 149 await waitFor(() => { 150 expect(ctxRef.current?.isLoading).toBe(false) 151 }) 152 153 expect(getPlugins).toHaveBeenCalledWith('mock-token') 154 expect(ctxRef.current?.plugins).toHaveLength(2) 155 expect(ctxRef.current?.plugins[0]!.name).toBe('analytics') 156 expect(ctxRef.current?.plugins[1]!.name).toBe('spam-filter') 157 }) 158 159 it('isPluginEnabled returns true for enabled plugin', async () => { 160 mockAuthAuthenticated() 161 vi.mocked(getPlugins).mockResolvedValue({ plugins: mockPlugins }) 162 163 const ctxRef = createContextRef() 164 render( 165 <PluginProvider> 166 <PluginConsumer ctxRef={ctxRef} /> 167 </PluginProvider> 168 ) 169 170 await waitFor(() => { 171 expect(ctxRef.current?.isLoading).toBe(false) 172 }) 173 174 expect(ctxRef.current?.isPluginEnabled('analytics')).toBe(true) 175 }) 176 177 it('isPluginEnabled returns false for disabled plugin', async () => { 178 mockAuthAuthenticated() 179 vi.mocked(getPlugins).mockResolvedValue({ plugins: mockPlugins }) 180 181 const ctxRef = createContextRef() 182 render( 183 <PluginProvider> 184 <PluginConsumer ctxRef={ctxRef} /> 185 </PluginProvider> 186 ) 187 188 await waitFor(() => { 189 expect(ctxRef.current?.isLoading).toBe(false) 190 }) 191 192 expect(ctxRef.current?.isPluginEnabled('spam-filter')).toBe(false) 193 }) 194 195 it('isPluginEnabled returns false for unknown plugin', async () => { 196 mockAuthAuthenticated() 197 vi.mocked(getPlugins).mockResolvedValue({ plugins: mockPlugins }) 198 199 const ctxRef = createContextRef() 200 render( 201 <PluginProvider> 202 <PluginConsumer ctxRef={ctxRef} /> 203 </PluginProvider> 204 ) 205 206 await waitFor(() => { 207 expect(ctxRef.current?.isLoading).toBe(false) 208 }) 209 210 expect(ctxRef.current?.isPluginEnabled('nonexistent')).toBe(false) 211 }) 212 213 it('getPluginSettings returns settings for installed plugin', async () => { 214 mockAuthAuthenticated() 215 vi.mocked(getPlugins).mockResolvedValue({ plugins: mockPlugins }) 216 217 const ctxRef = createContextRef() 218 render( 219 <PluginProvider> 220 <PluginConsumer ctxRef={ctxRef} /> 221 </PluginProvider> 222 ) 223 224 await waitFor(() => { 225 expect(ctxRef.current?.isLoading).toBe(false) 226 }) 227 228 const settings = ctxRef.current?.getPluginSettings('analytics') 229 expect(settings).toEqual({ trackPageViews: true, sampleRate: 0.5 }) 230 }) 231 232 it('getPluginSettings returns a copy, not the original object', async () => { 233 mockAuthAuthenticated() 234 vi.mocked(getPlugins).mockResolvedValue({ plugins: mockPlugins }) 235 236 const ctxRef = createContextRef() 237 render( 238 <PluginProvider> 239 <PluginConsumer ctxRef={ctxRef} /> 240 </PluginProvider> 241 ) 242 243 await waitFor(() => { 244 expect(ctxRef.current?.isLoading).toBe(false) 245 }) 246 247 const settings1 = ctxRef.current?.getPluginSettings('analytics') 248 const settings2 = ctxRef.current?.getPluginSettings('analytics') 249 expect(settings1).toEqual(settings2) 250 expect(settings1).not.toBe(settings2) 251 }) 252 253 it('getPluginSettings returns null for unknown plugin', async () => { 254 mockAuthAuthenticated() 255 vi.mocked(getPlugins).mockResolvedValue({ plugins: mockPlugins }) 256 257 const ctxRef = createContextRef() 258 render( 259 <PluginProvider> 260 <PluginConsumer ctxRef={ctxRef} /> 261 </PluginProvider> 262 ) 263 264 await waitFor(() => { 265 expect(ctxRef.current?.isLoading).toBe(false) 266 }) 267 268 expect(ctxRef.current?.getPluginSettings('nonexistent')).toBeNull() 269 }) 270 271 it('refreshPlugins triggers re-fetch', async () => { 272 mockAuthAuthenticated() 273 vi.mocked(getPlugins) 274 .mockResolvedValueOnce({ plugins: [mockPlugins[0]!] }) 275 .mockResolvedValueOnce({ plugins: mockPlugins }) 276 277 const ctxRef = createContextRef() 278 render( 279 <PluginProvider> 280 <PluginConsumer ctxRef={ctxRef} /> 281 </PluginProvider> 282 ) 283 284 // Wait for initial fetch 285 await waitFor(() => { 286 expect(ctxRef.current?.isLoading).toBe(false) 287 }) 288 expect(ctxRef.current?.plugins).toHaveLength(1) 289 290 // Trigger refresh 291 await act(async () => { 292 await ctxRef.current?.refreshPlugins() 293 }) 294 295 await waitFor(() => { 296 expect(ctxRef.current?.isLoading).toBe(false) 297 }) 298 299 expect(getPlugins).toHaveBeenCalledTimes(2) 300 expect(ctxRef.current?.plugins).toHaveLength(2) 301 }) 302 303 it('keeps existing plugins on fetch error', async () => { 304 mockAuthAuthenticated() 305 vi.mocked(getPlugins) 306 .mockResolvedValueOnce({ plugins: mockPlugins }) 307 .mockRejectedValueOnce(new Error('Network error')) 308 309 const ctxRef = createContextRef() 310 render( 311 <PluginProvider> 312 <PluginConsumer ctxRef={ctxRef} /> 313 </PluginProvider> 314 ) 315 316 // Wait for initial fetch 317 await waitFor(() => { 318 expect(ctxRef.current?.isLoading).toBe(false) 319 }) 320 expect(ctxRef.current?.plugins).toHaveLength(2) 321 322 // Trigger refresh that fails 323 await act(async () => { 324 await ctxRef.current?.refreshPlugins() 325 }) 326 327 await waitFor(() => { 328 expect(ctxRef.current?.isLoading).toBe(false) 329 }) 330 331 // Plugins should be preserved 332 expect(ctxRef.current?.plugins).toHaveLength(2) 333 }) 334 335 it('does not fetch while auth is still loading', () => { 336 vi.mocked(useAuth).mockReturnValue({ 337 user: null, 338 isAuthenticated: false, 339 isLoading: true, 340 crossPostScopesGranted: false, 341 getAccessToken: () => null, 342 login: vi.fn(), 343 logout: vi.fn(), 344 setSessionFromCallback: vi.fn(), 345 requestCrossPostAuth: vi.fn(), 346 authFetch: vi.fn(), 347 } as ReturnType<typeof useAuth>) 348 349 const ctxRef = createContextRef() 350 render( 351 <PluginProvider> 352 <PluginConsumer ctxRef={ctxRef} /> 353 </PluginProvider> 354 ) 355 356 expect(getPlugins).not.toHaveBeenCalled() 357 expect(ctxRef.current?.isLoading).toBe(true) 358 }) 359})