2024-09-10 18:18:46 +02:00
|
|
|
package notifications
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2025-01-29 20:21:52 +01:00
|
|
|
"errors"
|
2024-09-10 18:18:46 +02:00
|
|
|
"io"
|
|
|
|
|
"sync"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
2024-10-30 14:18:12 +01:00
|
|
|
"github.com/lightninglabs/aperture/l402"
|
2024-09-10 18:18:46 +02:00
|
|
|
"github.com/lightninglabs/loop/swapserverrpc"
|
2025-01-29 20:31:58 +01:00
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
2024-09-10 18:18:46 +02:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
testReservationId = []byte{0x01, 0x02}
|
|
|
|
|
testReservationId2 = []byte{0x01, 0x02}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// mockNotificationsClient implements the NotificationsClient interface for testing.
|
|
|
|
|
type mockNotificationsClient struct {
|
|
|
|
|
mockStream swapserverrpc.SwapServer_SubscribeNotificationsClient
|
|
|
|
|
subscribeErr error
|
2025-01-29 20:21:52 +01:00
|
|
|
attemptTimes []time.Time
|
2024-09-10 18:18:46 +02:00
|
|
|
timesCalled int
|
|
|
|
|
sync.Mutex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockNotificationsClient) SubscribeNotifications(ctx context.Context,
|
|
|
|
|
in *swapserverrpc.SubscribeNotificationsRequest,
|
|
|
|
|
opts ...grpc.CallOption) (
|
|
|
|
|
swapserverrpc.SwapServer_SubscribeNotificationsClient, error) {
|
|
|
|
|
|
|
|
|
|
m.Lock()
|
|
|
|
|
defer m.Unlock()
|
|
|
|
|
|
|
|
|
|
m.timesCalled++
|
2025-01-29 20:21:52 +01:00
|
|
|
m.attemptTimes = append(m.attemptTimes, time.Now())
|
2024-09-10 18:18:46 +02:00
|
|
|
if m.subscribeErr != nil {
|
|
|
|
|
return nil, m.subscribeErr
|
|
|
|
|
}
|
|
|
|
|
return m.mockStream, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mockSubscribeNotificationsClient simulates the server stream.
|
|
|
|
|
type mockSubscribeNotificationsClient struct {
|
|
|
|
|
grpc.ClientStream
|
|
|
|
|
recvChan chan *swapserverrpc.SubscribeNotificationsResponse
|
|
|
|
|
recvErrChan chan error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockSubscribeNotificationsClient) Recv() (
|
|
|
|
|
*swapserverrpc.SubscribeNotificationsResponse, error) {
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case err := <-m.recvErrChan:
|
|
|
|
|
return nil, err
|
|
|
|
|
case notif, ok := <-m.recvChan:
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, io.EOF
|
|
|
|
|
}
|
|
|
|
|
return notif, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockSubscribeNotificationsClient) Header() (metadata.MD, error) {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockSubscribeNotificationsClient) Trailer() metadata.MD {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockSubscribeNotificationsClient) CloseSend() error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockSubscribeNotificationsClient) Context() context.Context {
|
|
|
|
|
return context.TODO()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockSubscribeNotificationsClient) SendMsg(interface{}) error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *mockSubscribeNotificationsClient) RecvMsg(interface{}) error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-29 20:21:52 +01:00
|
|
|
// TestManager_ReservationNotification tests that the Manager correctly
|
|
|
|
|
// forwards reservation notifications to subscribers.
|
2024-09-10 18:18:46 +02:00
|
|
|
func TestManager_ReservationNotification(t *testing.T) {
|
2025-01-29 20:21:52 +01:00
|
|
|
t.Parallel()
|
|
|
|
|
|
2024-09-10 18:18:46 +02:00
|
|
|
// Create a mock notification client
|
|
|
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse, 1)
|
|
|
|
|
errChan := make(chan error, 1)
|
|
|
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
|
|
|
recvChan: recvChan,
|
|
|
|
|
recvErrChan: errChan,
|
|
|
|
|
}
|
|
|
|
|
mockClient := &mockNotificationsClient{
|
|
|
|
|
mockStream: mockStream,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a Manager with the mock client
|
|
|
|
|
mgr := NewManager(&Config{
|
|
|
|
|
Client: mockClient,
|
2024-10-30 14:18:12 +01:00
|
|
|
CurrentToken: func() (*l402.Token, error) {
|
2024-09-10 18:18:46 +02:00
|
|
|
// Simulate successful fetching of L402
|
2025-01-29 20:31:58 +01:00
|
|
|
return &l402.Token{
|
|
|
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
|
|
|
}, nil
|
2024-09-10 18:18:46 +02:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Subscribe to reservation notifications.
|
|
|
|
|
subCtx, subCancel := context.WithCancel(context.Background())
|
|
|
|
|
subChan := mgr.SubscribeReservations(subCtx)
|
|
|
|
|
|
|
|
|
|
// Run the manager.
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
err := mgr.Run(ctx)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Wait a bit to ensure manager is running and has subscribed
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
mgr.Lock()
|
|
|
|
|
defer mgr.Unlock()
|
|
|
|
|
return len(mgr.subscribers[NotificationTypeReservation]) > 0
|
|
|
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
|
|
|
|
|
|
|
|
mockClient.Lock()
|
|
|
|
|
require.Equal(t, 1, mockClient.timesCalled)
|
|
|
|
|
mockClient.Unlock()
|
|
|
|
|
|
|
|
|
|
// Send a test notification
|
|
|
|
|
testNotif := getTestNotification(testReservationId)
|
|
|
|
|
|
|
|
|
|
// Send the notification to the recvChan
|
|
|
|
|
recvChan <- testNotif
|
|
|
|
|
|
|
|
|
|
// Collect the notification in the callback
|
|
|
|
|
receivedNotification := <-subChan
|
|
|
|
|
|
|
|
|
|
// Now, check that the notification received in the callback matches the one sent
|
|
|
|
|
require.NotNil(t, receivedNotification)
|
|
|
|
|
require.Equal(t, testReservationId, receivedNotification.ReservationId)
|
|
|
|
|
|
|
|
|
|
// Cancel the subscription
|
|
|
|
|
subCancel()
|
|
|
|
|
|
|
|
|
|
// Send another test notification`
|
|
|
|
|
testNotif2 := getTestNotification(testReservationId2)
|
|
|
|
|
recvChan <- testNotif2
|
|
|
|
|
|
|
|
|
|
// Check that the subChan is eventually closed.
|
|
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
|
select {
|
|
|
|
|
case _, ok := <-subChan:
|
|
|
|
|
return !ok
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}, time.Second*5, 10*time.Millisecond)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getTestNotification(resId []byte) *swapserverrpc.SubscribeNotificationsResponse {
|
|
|
|
|
return &swapserverrpc.SubscribeNotificationsResponse{
|
|
|
|
|
Notification: &swapserverrpc.SubscribeNotificationsResponse_ReservationNotification{
|
|
|
|
|
ReservationNotification: &swapserverrpc.ServerReservationNotification{
|
|
|
|
|
ReservationId: resId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-29 20:21:52 +01:00
|
|
|
|
|
|
|
|
// TestManager_Backoff verifies that repeated failures in
|
|
|
|
|
// subscribeNotifications cause the Manager to space out subscription attempts
|
|
|
|
|
// via a predictable incremental backoff.
|
|
|
|
|
func TestManager_Backoff(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
// We'll tolerate a bit of jitter in the timing checks.
|
|
|
|
|
const tolerance = 300 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse)
|
|
|
|
|
recvErrChan := make(chan error)
|
|
|
|
|
|
|
|
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
|
|
|
recvChan: recvChan,
|
|
|
|
|
recvErrChan: recvErrChan,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a new mock client that will fail to subscribe.
|
|
|
|
|
mockClient := &mockNotificationsClient{
|
|
|
|
|
mockStream: mockStream,
|
|
|
|
|
subscribeErr: errors.New("failing on purpose"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manager with a successful CurrentToken so that it always tries
|
|
|
|
|
// to subscribe.
|
|
|
|
|
mgr := NewManager(&Config{
|
|
|
|
|
Client: mockClient,
|
|
|
|
|
CurrentToken: func() (*l402.Token, error) {
|
2025-01-29 20:31:58 +01:00
|
|
|
// Simulate successful fetching of L402
|
|
|
|
|
return &l402.Token{
|
|
|
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
|
|
|
}, nil
|
2025-01-29 20:21:52 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Run the manager in a background goroutine.
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
// We ignore the returned error because the Manager returns
|
|
|
|
|
// nil on context cancel.
|
|
|
|
|
_ = mgr.Run(ctx)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Wait long enough to see at least 3 subscription attempts using
|
|
|
|
|
// the Manager's default pattern.
|
|
|
|
|
// We'll wait ~5 seconds total so we capture at least 3 attempts:
|
|
|
|
|
// - Attempt #1: immediate
|
|
|
|
|
// - Attempt #2: ~1 second
|
|
|
|
|
// - Attempt #3: ~3 seconds after that etc.
|
|
|
|
|
time.Sleep(5 * time.Second)
|
|
|
|
|
|
2025-01-29 20:31:58 +01:00
|
|
|
// Cancel the context to stop the manager.
|
2025-01-29 20:21:52 +01:00
|
|
|
cancel()
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
|
|
// Check how many attempts we made.
|
|
|
|
|
require.GreaterOrEqual(t, len(mockClient.attemptTimes), 3,
|
|
|
|
|
"expected at least 3 attempts within 5 seconds",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expectedDelay := time.Second
|
|
|
|
|
for i := 1; i < len(mockClient.attemptTimes); i++ {
|
|
|
|
|
// The expected delay for the i-th gap (comparing attempt i to
|
|
|
|
|
// attempt i-1) is i seconds (because the manager increments
|
|
|
|
|
// the backoff by 1 second each time).
|
|
|
|
|
actualDelay := mockClient.attemptTimes[i].Sub(
|
|
|
|
|
mockClient.attemptTimes[i-1],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
require.InDeltaf(
|
|
|
|
|
t, expectedDelay, actualDelay, float64(tolerance),
|
|
|
|
|
"Attempt %d -> Attempt %d delay should be ~%v, got %v",
|
|
|
|
|
i, i+1, expectedDelay, actualDelay,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expectedDelay += time.Second
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestManager_MinAliveConnTime verifies that the Manager enforces the minimum
|
|
|
|
|
// alive connection time before considering a subscription successful.
|
|
|
|
|
func TestManager_MinAliveConnTime(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
// Tolerance to allow for scheduling jitter.
|
|
|
|
|
const tolerance = 300 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
// Set a small MinAliveConnTime so the test doesn't run too long.
|
|
|
|
|
// Once a subscription stays alive longer than 2s, the manager resets
|
|
|
|
|
// its backoff to 10s on the next loop iteration.
|
|
|
|
|
const minAlive = 1 * time.Second
|
|
|
|
|
|
|
|
|
|
// We'll provide a channel for incoming notifications
|
|
|
|
|
// and another for forcing errors to close the subscription.
|
|
|
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse)
|
|
|
|
|
recvErrChan := make(chan error)
|
|
|
|
|
|
|
|
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
|
|
|
recvChan: recvChan,
|
|
|
|
|
recvErrChan: recvErrChan,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No immediate error from SubscribeNotifications, so it "succeeds".
|
|
|
|
|
// We trigger subscription closure by sending an error to recvErrChan.
|
|
|
|
|
mockClient := &mockNotificationsClient{
|
|
|
|
|
mockStream: mockStream,
|
|
|
|
|
// subscribeErr stays nil => success on each call.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a Manager that uses our mock client and enforces
|
|
|
|
|
// MinAliveConnTime=2s.
|
|
|
|
|
mgr := NewManager(&Config{
|
|
|
|
|
Client: mockClient,
|
|
|
|
|
MinAliveConnTime: minAlive,
|
|
|
|
|
CurrentToken: func() (*l402.Token, error) {
|
2025-01-29 20:31:58 +01:00
|
|
|
// Simulate successful fetching of L402
|
|
|
|
|
return &l402.Token{
|
|
|
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
|
|
|
}, nil
|
2025-01-29 20:21:52 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
_ = mgr.Run(ctx)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Let the subscription stay alive for 2s, which is >1s (minAlive).
|
|
|
|
|
// Then force an error to end the subscription. The manager sees
|
|
|
|
|
// it stayed connected ~2s and resets its backoff to 10s.
|
|
|
|
|
go func() {
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
recvErrChan <- errors.New("mock subscription closed")
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Wait enough time (~13s) to see:
|
|
|
|
|
// - First subscription (2s)
|
|
|
|
|
// - Manager resets to 10s
|
|
|
|
|
// - Second subscription attempt starts ~10s later.
|
|
|
|
|
time.Sleep(13 * time.Second)
|
|
|
|
|
|
|
|
|
|
// Signal EOF so the subscription stops.
|
|
|
|
|
close(recvChan)
|
|
|
|
|
|
|
|
|
|
// Stop the manager and wait for cleanup.
|
|
|
|
|
cancel()
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
|
|
// Expect at least 2 attempts in attemptTimes:
|
|
|
|
|
// 1) The one that stayed alive for 2s,
|
|
|
|
|
// 2) The next attempt ~10s after that.
|
|
|
|
|
require.GreaterOrEqual(
|
|
|
|
|
t, len(mockClient.attemptTimes), 2,
|
|
|
|
|
"expected at least 2 attempts with a successful subscription",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
require.InDeltaf(
|
|
|
|
|
t, 12*time.Second,
|
|
|
|
|
mockClient.attemptTimes[1].Sub(mockClient.attemptTimes[0]),
|
|
|
|
|
float64(tolerance),
|
|
|
|
|
"Second attempt should occur ~2s after the first",
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-01-29 20:31:58 +01:00
|
|
|
|
|
|
|
|
// TestManager_Backoff_Pending_Token verifies that the Manager backs off when
|
|
|
|
|
// the token is pending.
|
|
|
|
|
func TestManager_Backoff_Pending_Token(t *testing.T) {
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
|
|
// We'll tolerate a bit of jitter in the timing checks.
|
|
|
|
|
const tolerance = 300 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
recvChan := make(chan *swapserverrpc.SubscribeNotificationsResponse)
|
|
|
|
|
recvErrChan := make(chan error)
|
|
|
|
|
|
|
|
|
|
mockStream := &mockSubscribeNotificationsClient{
|
|
|
|
|
recvChan: recvChan,
|
|
|
|
|
recvErrChan: recvErrChan,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a new mock client that will fail to subscribe.
|
|
|
|
|
mockClient := &mockNotificationsClient{
|
|
|
|
|
mockStream: mockStream,
|
|
|
|
|
// subscribeErr stays nil => would succeed on each call.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tokenCalls []time.Time
|
|
|
|
|
// Manager with a successful CurrentToken so that it always tries
|
|
|
|
|
// to subscribe.
|
|
|
|
|
mgr := NewManager(&Config{
|
|
|
|
|
Client: mockClient,
|
|
|
|
|
CurrentToken: func() (*l402.Token, error) {
|
|
|
|
|
tokenCalls = append(tokenCalls, time.Now())
|
|
|
|
|
if len(tokenCalls) < 3 {
|
|
|
|
|
// Simulate a pending token.
|
|
|
|
|
return &l402.Token{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simulate successful fetching of L402
|
|
|
|
|
return &l402.Token{
|
|
|
|
|
Preimage: lntypes.Preimage{1, 2, 3},
|
|
|
|
|
}, nil
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Run the manager in a background goroutine.
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
// We ignore the returned error because the Manager returns
|
|
|
|
|
// nil on context cancel.
|
|
|
|
|
_ = mgr.Run(ctx)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Wait long enough to see at least 3 token calls, so we can see that
|
|
|
|
|
// we'll indeed backoff when the token is pending.
|
|
|
|
|
time.Sleep(5 * time.Second)
|
|
|
|
|
|
|
|
|
|
// Signal EOF so the subscription stops.
|
|
|
|
|
close(recvChan)
|
|
|
|
|
|
|
|
|
|
// Cancel the context to stop the manager.
|
|
|
|
|
cancel()
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
|
|
// Expect exactly 3 token calls.
|
2025-03-06 14:26:11 +01:00
|
|
|
require.Len(t, tokenCalls, 3)
|
2025-01-29 20:31:58 +01:00
|
|
|
|
|
|
|
|
require.InDeltaf(
|
|
|
|
|
t, 3*time.Second, tokenCalls[2].Sub(tokenCalls[0]),
|
|
|
|
|
float64(tolerance),
|
|
|
|
|
"Expected to backoff for at ~3 seconds due to pending token",
|
|
|
|
|
)
|
|
|
|
|
}
|