Barazo AppView backend barazo.forum
at main 278 lines 8.6 kB view raw
1import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' 2import type { FastifyInstance } from 'fastify' 3import type { RegistryPlugin } from '../../../../src/lib/plugins/registry.js' 4import { 5 getRegistryIndex, 6 searchRegistryPlugins, 7 getFeaturedPlugins, 8} from '../../../../src/lib/plugins/registry.js' 9 10// --------------------------------------------------------------------------- 11// Test fixtures 12// --------------------------------------------------------------------------- 13 14function makePlugin(overrides: Partial<RegistryPlugin> = {}): RegistryPlugin { 15 return { 16 name: '@barazo/plugin-test', 17 displayName: 'Test Plugin', 18 description: 'A test plugin for unit tests', 19 version: '1.0.0', 20 source: 'official', 21 category: 'moderation', 22 barazoVersion: '^0.1.0', 23 author: { name: 'Barazo Team' }, 24 license: 'MIT', 25 npmUrl: 'https://www.npmjs.com/package/@barazo/plugin-test', 26 approved: true, 27 featured: false, 28 downloads: 100, 29 ...overrides, 30 } 31} 32 33const samplePlugins: RegistryPlugin[] = [ 34 makePlugin({ 35 name: '@barazo/plugin-polls', 36 displayName: 'Polls', 37 description: 'Add polls to your forum topics', 38 category: 'social', 39 source: 'official', 40 featured: true, 41 downloads: 500, 42 }), 43 makePlugin({ 44 name: '@barazo/plugin-spam-filter', 45 displayName: 'Spam Filter', 46 description: 'AI-powered spam detection', 47 category: 'moderation', 48 source: 'official', 49 featured: false, 50 downloads: 1200, 51 }), 52 makePlugin({ 53 name: 'community-badges', 54 displayName: 'Community Badges', 55 description: 'Custom badge system for community members', 56 category: 'social', 57 source: 'community', 58 featured: true, 59 downloads: 80, 60 }), 61 makePlugin({ 62 name: '@barazo/plugin-analytics', 63 displayName: 'Analytics Dashboard', 64 description: 'Privacy-friendly forum analytics', 65 category: 'admin', 66 source: 'official', 67 featured: false, 68 downloads: 300, 69 }), 70] 71 72// --------------------------------------------------------------------------- 73// searchRegistryPlugins 74// --------------------------------------------------------------------------- 75 76describe('searchRegistryPlugins', () => { 77 it('returns all plugins when no filters are provided', () => { 78 const results = searchRegistryPlugins(samplePlugins, {}) 79 expect(results).toHaveLength(4) 80 }) 81 82 it('filters by text query matching name', () => { 83 const results = searchRegistryPlugins(samplePlugins, { q: 'polls' }) 84 expect(results).toHaveLength(1) 85 expect(results[0]?.name).toBe('@barazo/plugin-polls') 86 }) 87 88 it('filters by text query matching displayName', () => { 89 const results = searchRegistryPlugins(samplePlugins, { q: 'Analytics Dashboard' }) 90 expect(results).toHaveLength(1) 91 expect(results[0]?.name).toBe('@barazo/plugin-analytics') 92 }) 93 94 it('filters by text query matching description', () => { 95 const results = searchRegistryPlugins(samplePlugins, { q: 'spam detection' }) 96 expect(results).toHaveLength(1) 97 expect(results[0]?.name).toBe('@barazo/plugin-spam-filter') 98 }) 99 100 it('text query is case-insensitive', () => { 101 const results = searchRegistryPlugins(samplePlugins, { q: 'POLLS' }) 102 expect(results).toHaveLength(1) 103 expect(results[0]?.name).toBe('@barazo/plugin-polls') 104 }) 105 106 it('filters by category', () => { 107 const results = searchRegistryPlugins(samplePlugins, { category: 'social' }) 108 expect(results).toHaveLength(2) 109 expect(results.map((p) => p.name)).toContain('@barazo/plugin-polls') 110 expect(results.map((p) => p.name)).toContain('community-badges') 111 }) 112 113 it('filters by source', () => { 114 const results = searchRegistryPlugins(samplePlugins, { source: 'community' }) 115 expect(results).toHaveLength(1) 116 expect(results[0]?.name).toBe('community-badges') 117 }) 118 119 it('combines query and category filters', () => { 120 const results = searchRegistryPlugins(samplePlugins, { q: 'badge', category: 'social' }) 121 expect(results).toHaveLength(1) 122 expect(results[0]?.name).toBe('community-badges') 123 }) 124 125 it('combines all three filters', () => { 126 const results = searchRegistryPlugins(samplePlugins, { 127 q: 'badge', 128 category: 'social', 129 source: 'community', 130 }) 131 expect(results).toHaveLength(1) 132 expect(results[0]?.name).toBe('community-badges') 133 }) 134 135 it('returns empty array when no plugins match', () => { 136 const results = searchRegistryPlugins(samplePlugins, { q: 'nonexistent-plugin-xyz' }) 137 expect(results).toHaveLength(0) 138 }) 139 140 it('returns empty array when category filter has no match', () => { 141 const results = searchRegistryPlugins(samplePlugins, { category: 'nonexistent' }) 142 expect(results).toHaveLength(0) 143 }) 144}) 145 146// --------------------------------------------------------------------------- 147// getFeaturedPlugins 148// --------------------------------------------------------------------------- 149 150describe('getFeaturedPlugins', () => { 151 it('returns only featured plugins', () => { 152 const results = getFeaturedPlugins(samplePlugins) 153 expect(results).toHaveLength(2) 154 expect(results.every((p) => p.featured)).toBe(true) 155 }) 156 157 it('returns empty array when no plugins are featured', () => { 158 const unfeatured = samplePlugins.map((p) => ({ ...p, featured: false })) 159 const results = getFeaturedPlugins(unfeatured) 160 expect(results).toHaveLength(0) 161 }) 162}) 163 164// --------------------------------------------------------------------------- 165// getRegistryIndex 166// --------------------------------------------------------------------------- 167 168describe('getRegistryIndex', () => { 169 const registryData = { 170 version: 1, 171 updatedAt: '2026-03-06T00:00:00Z', 172 plugins: samplePlugins, 173 } 174 175 function createMockApp(cacheValue: string | null = null): FastifyInstance { 176 return { 177 cache: { 178 get: vi.fn().mockResolvedValue(cacheValue), 179 set: vi.fn().mockResolvedValue('OK'), 180 }, 181 log: { 182 info: vi.fn(), 183 warn: vi.fn(), 184 error: vi.fn(), 185 debug: vi.fn(), 186 trace: vi.fn(), 187 fatal: vi.fn(), 188 child: vi.fn(), 189 }, 190 } as unknown as FastifyInstance 191 } 192 193 let originalFetch: typeof globalThis.fetch 194 195 beforeEach(() => { 196 originalFetch = globalThis.fetch 197 }) 198 199 afterEach(() => { 200 globalThis.fetch = originalFetch 201 vi.restoreAllMocks() 202 }) 203 204 it('returns plugins from cache when available', async () => { 205 const app = createMockApp(JSON.stringify(registryData)) 206 207 const result = await getRegistryIndex(app) 208 209 expect(result).toHaveLength(4) 210 expect(result[0]?.name).toBe('@barazo/plugin-polls') 211 // Should not have called fetch 212 // eslint-disable-next-line @typescript-eslint/unbound-method 213 expect(app.cache.get).toHaveBeenCalledWith('plugin:registry:index') 214 }) 215 216 it('fetches from registry when cache is empty', async () => { 217 const app = createMockApp(null) 218 219 globalThis.fetch = vi.fn().mockResolvedValue({ 220 ok: true, 221 json: vi.fn().mockResolvedValue(registryData), 222 }) 223 224 const result = await getRegistryIndex(app) 225 226 expect(result).toHaveLength(4) 227 expect(globalThis.fetch).toHaveBeenCalledWith( 228 'https://registry.barazo.forum/index.json', 229 expect.objectContaining({ signal: expect.any(AbortSignal) as AbortSignal }) 230 ) 231 // eslint-disable-next-line @typescript-eslint/unbound-method 232 expect(app.cache.set).toHaveBeenCalledWith( 233 'plugin:registry:index', 234 JSON.stringify(registryData), 235 'EX', 236 3600 237 ) 238 }) 239 240 it('returns empty array when fetch fails', async () => { 241 const app = createMockApp(null) 242 243 globalThis.fetch = vi.fn().mockResolvedValue({ 244 ok: false, 245 status: 503, 246 }) 247 248 const result = await getRegistryIndex(app) 249 250 expect(result).toHaveLength(0) 251 expect(app.log.warn).toHaveBeenCalled() 252 }) 253 254 it('returns empty array when fetch throws', async () => { 255 const app = createMockApp(null) 256 257 globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error')) 258 259 const result = await getRegistryIndex(app) 260 261 expect(result).toHaveLength(0) 262 expect(app.log.warn).toHaveBeenCalled() 263 }) 264 265 it('fetches fresh when cache contains invalid JSON', async () => { 266 const app = createMockApp('not-valid-json') 267 268 globalThis.fetch = vi.fn().mockResolvedValue({ 269 ok: true, 270 json: vi.fn().mockResolvedValue(registryData), 271 }) 272 273 const result = await getRegistryIndex(app) 274 275 expect(result).toHaveLength(4) 276 expect(globalThis.fetch).toHaveBeenCalled() 277 }) 278})