/** * Tests unit pour invalidateEntity — verifie les patterns generes par entite, * la cascade rollups parent, et l'idempotence (deux invalidations meme key). */ import { describe, expect, it } from 'vitest'; import { type CacheInvalidator, invalidateEntity } from '../../src/lib/cache.js'; class FakeRedis implements CacheInvalidator { public patterns: string[] = []; // Map pour simuler des keys persistees (incrementee a chaque set fictif). public callCount = 0; async invalidatePattern(pattern: string): Promise { this.patterns.push(pattern); this.callCount++; return 1; // un match fictif } } describe('invalidateEntity', () => { it('attribution : cascade sur module + personne (rollups RG-01)', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'attribution', 42); expect(redis.patterns).toContain('bridge:attribution:list:*'); expect(redis.patterns).toContain('bridge:attribution:row:42'); expect(redis.patterns).toContain('bridge:module:row:*'); expect(redis.patterns).toContain('bridge:module:list:*'); expect(redis.patterns).toContain('bridge:personne:row:*'); expect(redis.patterns).toContain('bridge:personne:list:*'); }); it('intervention : cascade sur tache + personne', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'intervention', 100); expect(redis.patterns).toContain('bridge:intervention:list:*'); expect(redis.patterns).toContain('bridge:intervention:row:100'); expect(redis.patterns).toContain('bridge:tache:row:*'); expect(redis.patterns).toContain('bridge:tache:list:*'); expect(redis.patterns).toContain('bridge:personne:row:*'); expect(redis.patterns).toContain('bridge:personne:list:*'); }); it('module : cascade sur bloc + formation', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'module', 7); expect(redis.patterns).toContain('bridge:module:list:*'); expect(redis.patterns).toContain('bridge:module:row:7'); expect(redis.patterns).toContain('bridge:bloc:row:*'); expect(redis.patterns).toContain('bridge:bloc:list:*'); expect(redis.patterns).toContain('bridge:formation:row:*'); expect(redis.patterns).toContain('bridge:formation:list:*'); }); it('bloc : cascade formation seulement', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'bloc', 3); expect(redis.patterns).toContain('bridge:bloc:list:*'); expect(redis.patterns).toContain('bridge:bloc:row:3'); expect(redis.patterns).toContain('bridge:formation:row:*'); expect(redis.patterns).toContain('bridge:formation:list:*'); // Pas de cascade modules au-dessus. expect(redis.patterns).not.toContain('bridge:module:list:*'); }); it('tache : cascade projet', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'tache', 8); expect(redis.patterns).toContain('bridge:tache:list:*'); expect(redis.patterns).toContain('bridge:tache:row:8'); expect(redis.patterns).toContain('bridge:projet:row:*'); expect(redis.patterns).toContain('bridge:projet:list:*'); }); it('projet : cascade client', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'projet', 5); expect(redis.patterns).toContain('bridge:projet:list:*'); expect(redis.patterns).toContain('bridge:projet:row:5'); expect(redis.patterns).toContain('bridge:client:row:*'); expect(redis.patterns).toContain('bridge:client:list:*'); }); it('personne : pas de cascade parent (entite racine)', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'personne', 1); expect(redis.patterns).toContain('bridge:personne:list:*'); expect(redis.patterns).toContain('bridge:personne:row:1'); expect(redis.patterns).toHaveLength(2); }); it('formation : pas de cascade parent (entite racine)', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'formation', 9); expect(redis.patterns).toContain('bridge:formation:list:*'); expect(redis.patterns).toContain('bridge:formation:row:9'); expect(redis.patterns).toHaveLength(2); }); it('client : pas de cascade parent', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'client', 4); expect(redis.patterns).toContain('bridge:client:list:*'); expect(redis.patterns).toContain('bridge:client:row:4'); expect(redis.patterns).toHaveLength(2); }); it('sans id : invalide juste la liste + cascade (cas create avant id connu)', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'attribution'); expect(redis.patterns).toContain('bridge:attribution:list:*'); expect(redis.patterns).not.toContain('bridge:attribution:row:undefined'); // Cascade toujours appliquee meme sans id. expect(redis.patterns).toContain('bridge:module:list:*'); }); it('idempotent : deux invalidations meme key ne throw pas', async () => { const redis = new FakeRedis(); await invalidateEntity(redis, 'attribution', 42); await expect(invalidateEntity(redis, 'attribution', 42)).resolves.toBeGreaterThanOrEqual(0); // Les patterns sont appeles deux fois, c'est attendu. expect(redis.patterns.filter((p) => p === 'bridge:attribution:row:42')).toHaveLength(2); }); it('retourne le total des keys invalidees', async () => { const redis = new FakeRedis(); const total = await invalidateEntity(redis, 'attribution', 1); // FakeRedis retourne 1 par appel, 6 patterns -> 6. expect(total).toBe(6); }); });