1+ import { describe , it , expect , jest , beforeEach , afterEach } from '@jest/globals' ;
2+ import type { MockedFunction } from 'jest-mock' ;
3+ import { Trigger } from '../src/triggers/base-trigger' ;
4+
5+ // Mock implementation of Trigger for testing
6+ class MockTrigger extends Trigger < any > {
7+ fetchData : MockedFunction < ( ) => Promise < any [ ] > > ;
8+ process : MockedFunction < ( ) => Promise < void > > ;
9+
10+ constructor ( id : string = 'test-trigger' , interval : number = 1000 ) {
11+ super ( id , interval ) ;
12+ this . fetchData = jest . fn < ( ) => Promise < any [ ] > > ( ) ;
13+ this . process = jest . fn < ( ) => Promise < void > > ( ) ;
14+ }
15+
16+ // Expose private methods for testing
17+ getConsecutiveFailures ( ) {
18+ return ( this as any ) . consecutiveFailures ;
19+ }
20+ }
21+
22+ describe ( 'BaseTrigger - Retry Logic' , ( ) => {
23+ let trigger : MockTrigger ;
24+
25+ beforeEach ( ( ) => {
26+ trigger = new MockTrigger ( 'test-trigger' , 100 ) ;
27+ jest . useFakeTimers ( ) ;
28+ } ) ;
29+
30+ afterEach ( ( ) => {
31+ trigger . stop ( ) ;
32+ jest . clearAllTimers ( ) ;
33+ jest . useRealTimers ( ) ;
34+ jest . clearAllMocks ( ) ;
35+ } ) ;
36+
37+ describe ( 'Retry with consecutive failures' , ( ) => {
38+ it ( 'should continue running after 3 consecutive failures' , async ( ) => {
39+ trigger . fetchData . mockRejectedValue ( new Error ( 'Test error' ) ) ;
40+
41+ trigger . start ( ) ;
42+
43+ // Simulate 3 failures
44+ for ( let i = 1 ; i <= 3 ; i ++ ) {
45+ jest . advanceTimersByTime ( 100 ) ;
46+ await Promise . resolve ( ) ;
47+ }
48+
49+ expect ( trigger . fetchData ) . toHaveBeenCalledTimes ( 3 ) ;
50+ expect ( trigger . getConsecutiveFailures ( ) ) . toBe ( 3 ) ;
51+
52+ // Trigger should still be running (timer not null)
53+ expect ( ( trigger as any ) . timer ) . not . toBeNull ( ) ;
54+ } ) ;
55+ } ) ;
56+
57+ describe ( 'Reset counter after success' , ( ) => {
58+ it ( 'should reset consecutive failures counter after successful execution' , async ( ) => {
59+ const mockData = [ { id : 1 } , { id : 2 } ] ;
60+
61+ // First 2 calls fail, third succeeds
62+ trigger . fetchData
63+ . mockRejectedValueOnce ( new Error ( 'Error 1' ) )
64+ . mockRejectedValueOnce ( new Error ( 'Error 2' ) )
65+ . mockResolvedValueOnce ( mockData ) ;
66+
67+ trigger . process . mockResolvedValue ( undefined ) ;
68+
69+ trigger . start ( ) ;
70+
71+ // First failure
72+ jest . advanceTimersByTime ( 100 ) ;
73+ await Promise . resolve ( ) ; //fetchData
74+ await Promise . resolve ( ) ; //process
75+ expect ( trigger . getConsecutiveFailures ( ) ) . toBe ( 1 ) ;
76+
77+ // Second failure
78+ jest . advanceTimersByTime ( 100 ) ;
79+ await Promise . resolve ( ) ; //fetchData
80+ await Promise . resolve ( ) ; //process
81+ expect ( trigger . getConsecutiveFailures ( ) ) . toBe ( 2 ) ;
82+
83+ // Third attempt succeeds
84+ jest . advanceTimersByTime ( 100 ) ;
85+ await Promise . resolve ( ) ; //fetchData
86+ await Promise . resolve ( ) ; //process
87+
88+ expect ( trigger . fetchData ) . toHaveBeenCalledTimes ( 3 ) ;
89+ expect ( trigger . process ) . toHaveBeenCalledWith ( mockData , undefined ) ;
90+ expect ( trigger . getConsecutiveFailures ( ) ) . toBe ( 0 ) ;
91+ } ) ;
92+ } ) ;
93+
94+ describe ( 'Stop after 5 consecutive failures' , ( ) => {
95+ it ( 'should stop the trigger after 5 consecutive failures' , async ( ) => {
96+ trigger . fetchData . mockRejectedValue ( new Error ( 'Persistent error' ) ) ;
97+
98+ trigger . start ( ) ;
99+
100+ // Simulate 5 failures
101+ for ( let i = 1 ; i <= 5 ; i ++ ) {
102+ jest . advanceTimersByTime ( 100 ) ;
103+ await Promise . resolve ( ) ; //fetchData
104+ await Promise . resolve ( ) ; //process
105+ }
106+
107+ expect ( trigger . fetchData ) . toHaveBeenCalledTimes ( 5 ) ;
108+ expect ( trigger . getConsecutiveFailures ( ) ) . toBe ( 5 ) ;
109+
110+ // Timer should be null (trigger stopped)
111+ expect ( ( trigger as any ) . timer ) . toBeNull ( ) ;
112+
113+ // Advancing time should not trigger more calls
114+ jest . advanceTimersByTime ( 100 ) ;
115+ await Promise . resolve ( ) ;
116+ expect ( trigger . fetchData ) . toHaveBeenCalledTimes ( 5 ) ; // Still 5, no new calls
117+ } ) ;
118+ } ) ;
119+
120+ describe ( 'Timer cleanup' , ( ) => {
121+ it ( 'should properly clean up timer when stop() is called' , async ( ) => {
122+ trigger . fetchData . mockResolvedValue ( [ ] ) ;
123+ trigger . process . mockResolvedValue ( undefined ) ;
124+
125+ trigger . start ( ) ;
126+ expect ( ( trigger as any ) . timer ) . not . toBeNull ( ) ;
127+
128+ await trigger . stop ( ) ;
129+
130+ // Should not make any calls after stop
131+ jest . advanceTimersByTime ( 100 ) ;
132+ await Promise . resolve ( ) ; //fetchData
133+ await Promise . resolve ( ) ; //process
134+ expect ( trigger . fetchData ) . not . toHaveBeenCalled ( ) ;
135+ } ) ;
136+ } ) ;
137+
138+ describe ( 'Error handling in process method' , ( ) => {
139+ it ( 'should handle errors from process() method and increment failure counter' , async ( ) => {
140+ const mockData = [ { id : 1 } ] ;
141+ trigger . fetchData . mockResolvedValue ( mockData ) ;
142+ trigger . process . mockRejectedValue ( new Error ( 'Process error' ) ) ;
143+
144+ trigger . start ( ) ;
145+
146+ jest . advanceTimersByTime ( 100 ) ;
147+ await Promise . resolve ( ) ; //fetchData
148+ await Promise . resolve ( ) ; //process
149+
150+ expect ( trigger . fetchData ) . toHaveBeenCalled ( ) ;
151+ expect ( trigger . process ) . toHaveBeenCalledWith ( mockData , undefined ) ;
152+ expect ( trigger . getConsecutiveFailures ( ) ) . toBe ( 1 ) ;
153+
154+ // Trigger should still be running
155+ expect ( ( trigger as any ) . timer ) . not . toBeNull ( ) ;
156+ } ) ;
157+ } ) ;
158+ } ) ;
0 commit comments