All files / services/scheduler-service/src/__tests__ scheduler-dedup.test.js

0% Statements 0/92
0% Branches 0/1
0% Functions 0/17
0% Lines 0/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158                                                                                                                                                                                                                                                                                                                           
jest.mock('@temporalio/activity', () => ({
  Context: { current: jest.fn() },
}));
 
jest.mock('../redis/client', () => ({ getRedisClient: jest.fn() }));
 
jest.mock('../models/Schedule', () => ({ findOne: jest.fn() }));
 
jest.mock('axios', () => ({ post: jest.fn() }));
 
jest.mock('mongoose', () => ({
  connection: { db: { collection: jest.fn() } },
  Types: { ObjectId: jest.fn((v) => v) },
}));
 
const { Context } = require('@temporalio/activity');
const { getRedisClient } = require('../redis/client');
const Schedule = require('../models/Schedule');
const axios = require('axios');
const mongoose = require('mongoose');
 
const { executeScheduledTrigger, buildDedupKey } = require('../temporal/activities');
 
const apiKeyFindOne = jest.fn();
 
function makeSchedule(overrides = {}) {
  return {
    type: 'crawler',
    isActive: true,
    apiKeyId: '507f1f77bcf86cd799439011',
    organizationId: 'org-1',
    targetId: 'target-1',
    payload: {},
    _id: 'sched-mongo-id',
    runCount: 0,
    lastRunAt: null,
    lastJobId: null,
    lastRunStatus: null,
    save: jest.fn().mockResolvedValue(true),
    ...overrides,
  };
}
 
let redis;
 
beforeEach(() => {
  jest.clearAllMocks();
  redis = { get: jest.fn(), set: jest.fn().mockResolvedValue('OK') };
  getRedisClient.mockReturnValue(redis);
  Context.current.mockReturnValue({ info: { workflowExecution: { runId: 'run-1' } } });
  mongoose.connection.db.collection.mockReturnValue({ findOne: apiKeyFindOne });
  apiKeyFindOne.mockResolvedValue({ keyValue: 'api-key-1' });
});
 
describe('buildDedupKey', () => {
  it('est déterministe et varie selon scheduleId et token', () => {
    expect(buildDedupKey('s1', 'r1')).toBe('scheduler:dedup:s1:r1');
    expect(buildDedupKey('s1', 'r1')).toBe(buildDedupKey('s1', 'r1'));
    expect(buildDedupKey('s1', 'r1')).not.toBe(buildDedupKey('s2', 'r1'));
    expect(buildDedupKey('s1', 'r1')).not.toBe(buildDedupKey('s1', 'r2'));
  });
});
 
describe('executeScheduledTrigger — idempotence au rejeu', () => {
  it('déclenche et mémorise le jobId au premier appel', async () => {
    const schedule = makeSchedule();
    Schedule.findOne.mockResolvedValue(schedule);
    axios.post.mockResolvedValue({ data: { data: { jobId: 'job-123' } } });
    redis.get.mockResolvedValue(null);
 
    const result = await executeScheduledTrigger('sched-1');
 
    expect(axios.post).toHaveBeenCalledTimes(1);
    expect(result).toEqual({ jobId: 'job-123', status: 'success' });
    expect(schedule.runCount).toBe(1);
    expect(schedule.lastJobId).toBe('job-123');
    expect(schedule.lastRunStatus).toBe('success');
    expect(redis.set).toHaveBeenCalledWith(
      buildDedupKey('sched-1', 'run-1'),
      'job-123',
      'EX',
      expect.any(Number),
    );
    expect(schedule.save).toHaveBeenCalled();
  });
 
  it('rejoue (même runId) : réutilise le jobId sans second POST', async () => {
    const schedule = makeSchedule();
    Schedule.findOne.mockResolvedValue(schedule);
    redis.get.mockResolvedValue('job-123');
 
    const result = await executeScheduledTrigger('sched-1');
 
    expect(axios.post).not.toHaveBeenCalled();
    expect(redis.set).not.toHaveBeenCalled();
    expect(result).toEqual({ deduped: true, jobId: 'job-123' });
    expect(schedule.runCount).toBe(0);
    expect(schedule.save).not.toHaveBeenCalled();
    expect(redis.get).toHaveBeenCalledWith(buildDedupKey('sched-1', 'run-1'));
  });
 
  it('un runId différent déclenche un nouveau job', async () => {
    const schedule = makeSchedule();
    Schedule.findOne.mockResolvedValue(schedule);
    axios.post.mockResolvedValue({ data: { data: { jobId: 'job-456' } } });
    Context.current.mockReturnValue({ info: { workflowExecution: { runId: 'run-2' } } });
    redis.get.mockResolvedValue(null);
 
    await executeScheduledTrigger('sched-1');
 
    expect(axios.post).toHaveBeenCalledTimes(1);
    expect(redis.set).toHaveBeenCalledWith(
      buildDedupKey('sched-1', 'run-2'),
      'job-456',
      'EX',
      expect.any(Number),
    );
  });
 
  it("marque l'échec sans écrire de clé si le POST échoue", async () => {
    const schedule = makeSchedule();
    Schedule.findOne.mockResolvedValue(schedule);
    axios.post.mockRejectedValue(new Error('gateway down'));
    redis.get.mockResolvedValue(null);
 
    const result = await executeScheduledTrigger('sched-1');
 
    expect(schedule.lastRunStatus).toBe('failed');
    expect(schedule.lastJobId).toBeNull();
    expect(redis.set).not.toHaveBeenCalled();
    expect(result.status).toBe('failed');
  });
 
  it('ne déclenche pas si la planification est en pause', async () => {
    const schedule = makeSchedule({ isActive: false });
    Schedule.findOne.mockResolvedValue(schedule);
 
    const result = await executeScheduledTrigger('sched-1');
 
    expect(axios.post).not.toHaveBeenCalled();
    expect(result).toEqual({ skipped: 'paused' });
  });
 
  it('déclenche quand même si Redis est indisponible (dédup best-effort)', async () => {
    const schedule = makeSchedule();
    Schedule.findOne.mockResolvedValue(schedule);
    axios.post.mockResolvedValue({ data: { data: { jobId: 'job-789' } } });
    redis.get.mockRejectedValue(new Error('redis down'));
    redis.set.mockRejectedValue(new Error('redis down'));
 
    const result = await executeScheduledTrigger('sched-1');
 
    expect(axios.post).toHaveBeenCalledTimes(1);
    expect(result.jobId).toBe('job-789');
    expect(schedule.lastRunStatus).toBe('success');
  });
});