@@ -2,6 +2,7 @@ package notifications
22
33import (
44 "context"
5+ "errors"
56 "io"
67 "sync"
78 "testing"
2324type mockNotificationsClient struct {
2425 mockStream swapserverrpc.SwapServer_SubscribeNotificationsClient
2526 subscribeErr error
27+ attemptTimes []time.Time
2628 timesCalled int
2729 sync.Mutex
2830}
@@ -36,6 +38,7 @@ func (m *mockNotificationsClient) SubscribeNotifications(ctx context.Context,
3638 defer m .Unlock ()
3739
3840 m .timesCalled ++
41+ m .attemptTimes = append (m .attemptTimes , time .Now ())
3942 if m .subscribeErr != nil {
4043 return nil , m .subscribeErr
4144 }
@@ -87,7 +90,11 @@ func (m *mockSubscribeNotificationsClient) RecvMsg(interface{}) error {
8790 return nil
8891}
8992
93+ // TestManager_ReservationNotification tests that the Manager correctly
94+ // forwards reservation notifications to subscribers.
9095func TestManager_ReservationNotification (t * testing.T ) {
96+ t .Parallel ()
97+
9198 // Create a mock notification client
9299 recvChan := make (chan * swapserverrpc.SubscribeNotificationsResponse , 1 )
93100 errChan := make (chan error , 1 )
@@ -172,3 +179,169 @@ func getTestNotification(resId []byte) *swapserverrpc.SubscribeNotificationsResp
172179 },
173180 }
174181}
182+
183+ // TestManager_Backoff verifies that repeated failures in
184+ // subscribeNotifications cause the Manager to space out subscription attempts
185+ // via a predictable incremental backoff.
186+ func TestManager_Backoff (t * testing.T ) {
187+ t .Parallel ()
188+
189+ // We'll tolerate a bit of jitter in the timing checks.
190+ const tolerance = 300 * time .Millisecond
191+
192+ recvChan := make (chan * swapserverrpc.SubscribeNotificationsResponse )
193+ recvErrChan := make (chan error )
194+
195+ mockStream := & mockSubscribeNotificationsClient {
196+ recvChan : recvChan ,
197+ recvErrChan : recvErrChan ,
198+ }
199+
200+ // Create a new mock client that will fail to subscribe.
201+ mockClient := & mockNotificationsClient {
202+ mockStream : mockStream ,
203+ subscribeErr : errors .New ("failing on purpose" ),
204+ }
205+
206+ // Manager with a successful CurrentToken so that it always tries
207+ // to subscribe.
208+ mgr := NewManager (& Config {
209+ Client : mockClient ,
210+ CurrentToken : func () (* l402.Token , error ) {
211+ return & l402.Token {}, nil
212+ },
213+ })
214+
215+ // Run the manager in a background goroutine.
216+ ctx , cancel := context .WithCancel (context .Background ())
217+ defer cancel ()
218+
219+ var wg sync.WaitGroup
220+ wg .Add (1 )
221+ go func () {
222+ defer wg .Done ()
223+ // We ignore the returned error because the Manager returns
224+ // nil on context cancel.
225+ _ = mgr .Run (ctx )
226+ }()
227+
228+ // Wait long enough to see at least 3 subscription attempts using
229+ // the Manager's default pattern.
230+ // We'll wait ~5 seconds total so we capture at least 3 attempts:
231+ // - Attempt #1: immediate
232+ // - Attempt #2: ~1 second
233+ // - Attempt #3: ~3 seconds after that etc.
234+ time .Sleep (5 * time .Second )
235+
236+ // Cancel the contedt to stop the manager.
237+ cancel ()
238+ wg .Wait ()
239+
240+ // Check how many attempts we made.
241+ require .GreaterOrEqual (t , len (mockClient .attemptTimes ), 3 ,
242+ "expected at least 3 attempts within 5 seconds" ,
243+ )
244+
245+ expectedDelay := time .Second
246+ for i := 1 ; i < len (mockClient .attemptTimes ); i ++ {
247+ // The expected delay for the i-th gap (comparing attempt i to
248+ // attempt i-1) is i seconds (because the manager increments
249+ // the backoff by 1 second each time).
250+ actualDelay := mockClient .attemptTimes [i ].Sub (
251+ mockClient .attemptTimes [i - 1 ],
252+ )
253+
254+ require .InDeltaf (
255+ t , expectedDelay , actualDelay , float64 (tolerance ),
256+ "Attempt %d -> Attempt %d delay should be ~%v, got %v" ,
257+ i , i + 1 , expectedDelay , actualDelay ,
258+ )
259+
260+ expectedDelay += time .Second
261+ }
262+ }
263+
264+ // TestManager_MinAliveConnTime verifies that the Manager enforces the minimum
265+ // alive connection time before considering a subscription successful.
266+ func TestManager_MinAliveConnTime (t * testing.T ) {
267+ t .Parallel ()
268+
269+ // Tolerance to allow for scheduling jitter.
270+ const tolerance = 300 * time .Millisecond
271+
272+ // Set a small MinAliveConnTime so the test doesn't run too long.
273+ // Once a subscription stays alive longer than 2s, the manager resets
274+ // its backoff to 10s on the next loop iteration.
275+ const minAlive = 1 * time .Second
276+
277+ // We'll provide a channel for incoming notifications
278+ // and another for forcing errors to close the subscription.
279+ recvChan := make (chan * swapserverrpc.SubscribeNotificationsResponse )
280+ recvErrChan := make (chan error )
281+
282+ mockStream := & mockSubscribeNotificationsClient {
283+ recvChan : recvChan ,
284+ recvErrChan : recvErrChan ,
285+ }
286+
287+ // No immediate error from SubscribeNotifications, so it "succeeds".
288+ // We trigger subscription closure by sending an error to recvErrChan.
289+ mockClient := & mockNotificationsClient {
290+ mockStream : mockStream ,
291+ // subscribeErr stays nil => success on each call.
292+ }
293+
294+ // Create a Manager that uses our mock client and enforces
295+ // MinAliveConnTime=2s.
296+ mgr := NewManager (& Config {
297+ Client : mockClient ,
298+ MinAliveConnTime : minAlive ,
299+ CurrentToken : func () (* l402.Token , error ) {
300+ return & l402.Token {}, nil
301+ },
302+ })
303+
304+ ctx , cancel := context .WithCancel (context .Background ())
305+ var wg sync.WaitGroup
306+ wg .Add (1 )
307+ go func () {
308+ defer wg .Done ()
309+ _ = mgr .Run (ctx )
310+ }()
311+
312+ // Let the subscription stay alive for 2s, which is >1s (minAlive).
313+ // Then force an error to end the subscription. The manager sees
314+ // it stayed connected ~2s and resets its backoff to 10s.
315+ go func () {
316+ time .Sleep (2 * time .Second )
317+ recvErrChan <- errors .New ("mock subscription closed" )
318+ }()
319+
320+ // Wait enough time (~13s) to see:
321+ // - First subscription (2s)
322+ // - Manager resets to 10s
323+ // - Second subscription attempt starts ~10s later.
324+ time .Sleep (13 * time .Second )
325+
326+ // Signal EOF so the subscription stops.
327+ close (recvChan )
328+
329+ // Stop the manager and wait for cleanup.
330+ cancel ()
331+ wg .Wait ()
332+
333+ // Expect at least 2 attempts in attemptTimes:
334+ // 1) The one that stayed alive for 2s,
335+ // 2) The next attempt ~10s after that.
336+ require .GreaterOrEqual (
337+ t , len (mockClient .attemptTimes ), 2 ,
338+ "expected at least 2 attempts with a successful subscription" ,
339+ )
340+
341+ require .InDeltaf (
342+ t , 12 * time .Second ,
343+ mockClient .attemptTimes [1 ].Sub (mockClient .attemptTimes [0 ]),
344+ float64 (tolerance ),
345+ "Second attempt should occur ~2s after the first" ,
346+ )
347+ }
0 commit comments