Wiki/bridge/tests/unit/cache.test.ts
Corentin JOGUET 0cf6533885
Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
feat(bridge): Bloc 5 rate limit + cache invalidation cote writes
2026-05-07 21:44:33 +02:00

134 lines
5.5 KiB
TypeScript

/**
* 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<number> {
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);
});
});