File size: 2,223 Bytes
1f5ea39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
 * Idempotency key TTL cleanup (H6).
 *
 * The TREK client replays queued mutations with their X-Idempotency-Key on
 * reconnect, so the server must keep keys long enough to cover a realistic
 * offline window — otherwise a key GC'd before the device returns lets the
 * replay create a duplicate. The TTL was raised from 24h to 30d (overridable).
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { db } from '../../src/db/database';
import { purgeExpiredIdempotencyKeys } from '../../src/scheduler';

const DAY = 24 * 60 * 60;
const NOW = 2_000_000_000_000; // fixed ms so the test is deterministic
const NOW_SEC = Math.floor(NOW / 1000);

function insertKey(key: string, ageSeconds: number): void {
  db.prepare(
    `INSERT INTO idempotency_keys (key, user_id, method, path, status_code, response_body, created_at)
     VALUES (?, 1, 'POST', '/x', 200, '{}', ?)`,
  ).run(key, NOW_SEC - ageSeconds);
}

beforeEach(() => {
  db.pragma('foreign_keys = OFF'); // fixtures reference a user we don't seed here
  db.prepare('DELETE FROM idempotency_keys').run();
});

afterEach(() => {
  db.prepare('DELETE FROM idempotency_keys').run();
  db.pragma('foreign_keys = ON');
  delete process.env.IDEMPOTENCY_TTL_SECONDS;
});

describe('purgeExpiredIdempotencyKeys', () => {
  it('removes keys older than the 30-day default, keeps recent ones', () => {
    insertKey('old', 31 * DAY);
    insertKey('fresh', 5 * DAY);

    const removed = purgeExpiredIdempotencyKeys(NOW, undefined, db);

    expect(removed).toBe(1);
    const keys = db.prepare('SELECT key FROM idempotency_keys').all().map((r: { key: string }) => r.key);
    expect(keys).toEqual(['fresh']);
  });

  it('keeps a 25-day-old key that the old 24h TTL would have dropped', () => {
    insertKey('offline-trip', 25 * DAY);
    expect(purgeExpiredIdempotencyKeys(NOW, undefined, db)).toBe(0);
    expect(db.prepare('SELECT COUNT(*) c FROM idempotency_keys').get()).toMatchObject({ c: 1 });
  });

  it('respects the IDEMPOTENCY_TTL_SECONDS override', () => {
    process.env.IDEMPOTENCY_TTL_SECONDS = String(DAY);
    insertKey('twoDays', 2 * DAY);
    expect(purgeExpiredIdempotencyKeys(NOW, undefined, db)).toBe(1);
  });
});