github.com/livekit/protocol@v1.39.3/webhook/webhook_test.go (about)

     1  // Copyright 2023 LiveKit, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package webhook
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net"
    21  	"net/http"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/prometheus/client_golang/prometheus"
    27  	"github.com/stretchr/testify/require"
    28  	"go.uber.org/atomic"
    29  
    30  	"github.com/livekit/protocol/auth"
    31  	"github.com/livekit/protocol/livekit"
    32  )
    33  
    34  const (
    35  	testAPIKey           = "mykey"
    36  	testAPISecret        = "mysecret"
    37  	testAddr             = ":8765"
    38  	testUrl              = "http://localhost:8765"
    39  	webhookCheckInterval = 100 * time.Millisecond
    40  )
    41  
    42  var authProvider = auth.NewSimpleKeyProvider(
    43  	testAPIKey, testAPISecret,
    44  )
    45  
    46  func TestWebHook(t *testing.T) {
    47  	InitWebhookStats(prometheus.Labels{})
    48  
    49  	s := newServer(testAddr)
    50  	require.NoError(t, s.Start())
    51  	defer s.Stop()
    52  
    53  	t.Run("test event payload", func(t *testing.T) {
    54  		notifier := newTestNotifier()
    55  		defer notifier.Stop(false)
    56  
    57  		event := &livekit.WebhookEvent{
    58  			Event: EventTrackPublished,
    59  			Participant: &livekit.ParticipantInfo{
    60  				Identity: "test",
    61  			},
    62  			Track: &livekit.TrackInfo{
    63  				Sid: "TR_abcde",
    64  			},
    65  		}
    66  
    67  		wg := sync.WaitGroup{}
    68  		wg.Add(1)
    69  		expectedUrl := "/"
    70  		s.handler = func(w http.ResponseWriter, r *http.Request) {
    71  			defer wg.Done()
    72  			decodedEvent, err := ReceiveWebhookEvent(r, authProvider)
    73  			require.NoError(t, err)
    74  
    75  			require.EqualValues(t, event, decodedEvent)
    76  			require.Equal(t, expectedUrl, r.URL.String())
    77  		}
    78  		require.NoError(t, notifier.QueueNotify(context.Background(), event))
    79  		wg.Wait()
    80  
    81  		wg.Add(1)
    82  		expectedUrl = "/wh"
    83  		require.NoError(t, notifier.QueueNotify(context.Background(), event, WithExtraWebhooks([]*livekit.WebhookConfig{&livekit.WebhookConfig{Url: "http://localhost:8765/wh"}})))
    84  		wg.Wait()
    85  
    86  	})
    87  }
    88  
    89  func TestURLNotifierDropped(t *testing.T) {
    90  	InitWebhookStats(prometheus.Labels{})
    91  
    92  	s := newServer(testAddr)
    93  	require.NoError(t, s.Start())
    94  	defer s.Stop()
    95  
    96  	urlNotifier := newTestNotifier()
    97  	defer urlNotifier.Stop(true)
    98  	totalDropped := atomic.Int32{}
    99  	totalReceived := atomic.Int32{}
   100  	s.handler = func(w http.ResponseWriter, r *http.Request) {
   101  		decodedEvent, err := ReceiveWebhookEvent(r, authProvider)
   102  		require.NoError(t, err)
   103  		totalReceived.Inc()
   104  		totalDropped.Add(decodedEvent.NumDropped)
   105  	}
   106  	// send multiple notifications
   107  	for i := 0; i < 10; i++ {
   108  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   109  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventParticipantJoined})
   110  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   111  	}
   112  
   113  	time.Sleep(webhookCheckInterval)
   114  
   115  	require.Equal(t, int32(30), totalDropped.Load()+totalReceived.Load())
   116  	// at least one request dropped
   117  	require.Less(t, int32(0), totalDropped.Load())
   118  }
   119  
   120  func TestURLNotifierLifecycle(t *testing.T) {
   121  	InitWebhookStats(prometheus.Labels{})
   122  
   123  	s := newServer(testAddr)
   124  	require.NoError(t, s.Start())
   125  	defer s.Stop()
   126  
   127  	t.Run("start/stop without use", func(t *testing.T) {
   128  		urlNotifier := newTestNotifier()
   129  		urlNotifier.Stop(false)
   130  	})
   131  
   132  	t.Run("stop allowing to drain", func(t *testing.T) {
   133  		urlNotifier := newTestNotifier()
   134  		numCalled := atomic.Int32{}
   135  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   136  			numCalled.Inc()
   137  		}
   138  		for i := 0; i < 10; i++ {
   139  			_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   140  			_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   141  		}
   142  		urlNotifier.Stop(false)
   143  		require.Eventually(t, func() bool { return numCalled.Load() == 20 }, 5*time.Second, webhookCheckInterval)
   144  	})
   145  
   146  	t.Run("force stop", func(t *testing.T) {
   147  		urlNotifier := newTestNotifier()
   148  		numCalled := atomic.Int32{}
   149  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   150  			numCalled.Inc()
   151  		}
   152  		for i := 0; i < 10; i++ {
   153  			_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   154  			_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   155  		}
   156  		urlNotifier.Stop(true)
   157  		time.Sleep(time.Second)
   158  		require.Greater(t, int32(20), numCalled.Load())
   159  	})
   160  
   161  	t.Run("times out after accepting connection", func(t *testing.T) {
   162  		urlNotifier := NewURLNotifier(URLNotifierParams{
   163  			URL:       testUrl,
   164  			APIKey:    testAPIKey,
   165  			APISecret: testAPISecret,
   166  			HTTPClientParams: HTTPClientParams{
   167  				RetryWaitMax:  time.Millisecond,
   168  				MaxRetries:    1,
   169  				ClientTimeout: 100 * time.Millisecond,
   170  			},
   171  			Config: URLNotifierConfig{
   172  				QueueSize: 20,
   173  			},
   174  		})
   175  
   176  		numCalled := atomic.Int32{}
   177  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   178  			w.WriteHeader(200)
   179  			w.Write([]byte("ok"))
   180  
   181  			// delay the request to cause it to fail
   182  			time.Sleep(time.Second)
   183  			if r.Context().Err() == nil {
   184  				// inc if not canceled
   185  				numCalled.Inc()
   186  			}
   187  		}
   188  		defer urlNotifier.Stop(false)
   189  
   190  		err := urlNotifier.send(&livekit.WebhookEvent{Event: EventRoomStarted}, &urlNotifier.params)
   191  		require.Error(t, err)
   192  	})
   193  
   194  	t.Run("times out before connection", func(t *testing.T) {
   195  		ln, err := net.Listen("tcp", ":9987")
   196  		require.NoError(t, err)
   197  		defer ln.Close()
   198  		urlNotifier := NewURLNotifier(URLNotifierParams{
   199  			URL:       "http://localhost:9987",
   200  			APIKey:    testAPIKey,
   201  			APISecret: testAPISecret,
   202  			HTTPClientParams: HTTPClientParams{
   203  				RetryWaitMax:  time.Millisecond,
   204  				MaxRetries:    1,
   205  				ClientTimeout: 100 * time.Millisecond,
   206  			},
   207  		})
   208  		defer urlNotifier.Stop(false)
   209  
   210  		startedAt := time.Now()
   211  		err = urlNotifier.send(&livekit.WebhookEvent{Event: EventRoomStarted}, &urlNotifier.params)
   212  		require.Error(t, err)
   213  		require.Less(t, time.Since(startedAt).Seconds(), float64(2))
   214  	})
   215  }
   216  
   217  func TestURLNotifierFilter(t *testing.T) {
   218  	InitWebhookStats(prometheus.Labels{})
   219  
   220  	s := newServer(testAddr)
   221  	require.NoError(t, s.Start())
   222  	defer s.Stop()
   223  
   224  	t.Run("none", func(t *testing.T) {
   225  		urlNotifier := NewURLNotifier(URLNotifierParams{
   226  			URL:       testUrl,
   227  			APIKey:    testAPIKey,
   228  			APISecret: testAPISecret,
   229  			Config: URLNotifierConfig{
   230  				QueueSize: 20,
   231  			},
   232  		})
   233  		defer urlNotifier.Stop(false)
   234  
   235  		numCalled := atomic.Int32{}
   236  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   237  			numCalled.Inc()
   238  		}
   239  
   240  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   241  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   242  		require.Eventually(
   243  			t,
   244  			func() bool {
   245  				return numCalled.Load() == 2
   246  			},
   247  			5*time.Second,
   248  			webhookCheckInterval,
   249  		)
   250  	})
   251  
   252  	t.Run("includes", func(t *testing.T) {
   253  		urlNotifier := NewURLNotifier(URLNotifierParams{
   254  			URL:       testUrl,
   255  			APIKey:    testAPIKey,
   256  			APISecret: testAPISecret,
   257  			FilterParams: FilterParams{
   258  				IncludeEvents: []string{EventRoomStarted},
   259  			},
   260  			Config: URLNotifierConfig{
   261  				QueueSize: 20,
   262  			},
   263  		})
   264  		defer urlNotifier.Stop(false)
   265  
   266  		numCalled := atomic.Int32{}
   267  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   268  			numCalled.Inc()
   269  		}
   270  
   271  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   272  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   273  		require.Eventually(
   274  			t,
   275  			func() bool {
   276  				return numCalled.Load() == 1
   277  			},
   278  			5*time.Second,
   279  			webhookCheckInterval,
   280  		)
   281  	})
   282  
   283  	t.Run("excludes", func(t *testing.T) {
   284  		urlNotifier := NewURLNotifier(URLNotifierParams{
   285  			URL:       testUrl,
   286  			APIKey:    testAPIKey,
   287  			APISecret: testAPISecret,
   288  			FilterParams: FilterParams{
   289  				ExcludeEvents: []string{EventRoomStarted},
   290  			},
   291  			Config: URLNotifierConfig{
   292  				QueueSize: 20,
   293  			},
   294  		})
   295  		defer urlNotifier.Stop(false)
   296  
   297  		numCalled := atomic.Int32{}
   298  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   299  			numCalled.Inc()
   300  		}
   301  
   302  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   303  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   304  		require.Eventually(
   305  			t,
   306  			func() bool {
   307  				return numCalled.Load() == 1
   308  			},
   309  			5*time.Second,
   310  			webhookCheckInterval,
   311  		)
   312  	})
   313  
   314  	t.Run("includes + excludes", func(t *testing.T) {
   315  		urlNotifier := NewURLNotifier(URLNotifierParams{
   316  			URL:       testUrl,
   317  			APIKey:    testAPIKey,
   318  			APISecret: testAPISecret,
   319  			FilterParams: FilterParams{
   320  				IncludeEvents: []string{EventRoomStarted},
   321  				ExcludeEvents: []string{EventRoomStarted, EventRoomFinished},
   322  			},
   323  			Config: URLNotifierConfig{
   324  				QueueSize: 20,
   325  			},
   326  		})
   327  		defer urlNotifier.Stop(false)
   328  
   329  		numCalled := atomic.Int32{}
   330  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   331  			numCalled.Inc()
   332  		}
   333  
   334  		// EventRoomStarted should be allowed as IncludeEvents take precedence
   335  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   336  		_ = urlNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   337  		require.Eventually(
   338  			t,
   339  			func() bool {
   340  				return numCalled.Load() == 1
   341  			},
   342  			5*time.Second,
   343  			webhookCheckInterval,
   344  		)
   345  	})
   346  }
   347  
   348  func newTestNotifier() *URLNotifier {
   349  	return NewURLNotifier(URLNotifierParams{
   350  		URL:       testUrl,
   351  		APIKey:    testAPIKey,
   352  		APISecret: testAPISecret,
   353  		Config: URLNotifierConfig{
   354  			QueueSize: 20,
   355  		},
   356  	})
   357  }
   358  
   359  // --------------------------------------------
   360  
   361  func TestResourceWebHook(t *testing.T) {
   362  	s := newServer(testAddr)
   363  	require.NoError(t, s.Start())
   364  	defer s.Stop()
   365  
   366  	t.Run("test event payload", func(t *testing.T) {
   367  		resourceURLNotifier, err := NewDefaultNotifier(
   368  			WebHookConfig{
   369  				URLs:   []string{testUrl},
   370  				APIKey: testAPIKey,
   371  			},
   372  			authProvider,
   373  		)
   374  		require.NoError(t, err)
   375  		defer resourceURLNotifier.Stop(false)
   376  
   377  		event := &livekit.WebhookEvent{
   378  			Event: EventTrackPublished,
   379  			Participant: &livekit.ParticipantInfo{
   380  				Identity: "test",
   381  			},
   382  			Track: &livekit.TrackInfo{
   383  				Sid: "TR_abcde",
   384  			},
   385  		}
   386  
   387  		wg := sync.WaitGroup{}
   388  		wg.Add(1)
   389  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   390  			defer wg.Done()
   391  			decodedEvent, err := ReceiveWebhookEvent(r, authProvider)
   392  			require.NoError(t, err)
   393  
   394  			require.EqualValues(t, event, decodedEvent)
   395  		}
   396  		require.NoError(t, resourceURLNotifier.QueueNotify(context.Background(), event))
   397  		wg.Wait()
   398  	})
   399  
   400  }
   401  
   402  func TestResourceURLNotifierDropped(t *testing.T) {
   403  	s := newServer(testAddr)
   404  	require.NoError(t, s.Start())
   405  	defer s.Stop()
   406  
   407  	t.Run("depth drop", func(t *testing.T) {
   408  		resourceURLNotifier := newTestResourceNotifier(time.Minute, time.Minute, 5)
   409  		defer resourceURLNotifier.Stop(true)
   410  		totalDropped := atomic.Int32{}
   411  		totalReceived := atomic.Int32{}
   412  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   413  			_, err := ReceiveWebhookEvent(r, authProvider)
   414  			require.NoError(t, err)
   415  			totalReceived.Inc()
   416  		}
   417  		// send multiple notifications
   418  		for i := 0; i < 10; i++ {
   419  			err := resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   420  			if err == errQueueFull {
   421  				totalDropped.Inc()
   422  			}
   423  			err = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventParticipantJoined})
   424  			if err == errQueueFull {
   425  				totalDropped.Inc()
   426  			}
   427  			err = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   428  			if err == errQueueFull {
   429  				totalDropped.Inc()
   430  			}
   431  		}
   432  
   433  		time.Sleep(webhookCheckInterval)
   434  
   435  		require.Eventually(
   436  			t,
   437  			func() bool {
   438  				return totalDropped.Load()+totalReceived.Load() == 30
   439  			},
   440  			5*time.Second,
   441  			webhookCheckInterval,
   442  		)
   443  		// at least one request dropped, but not all dropped
   444  		require.Less(t, int32(0), totalDropped.Load())
   445  		require.Less(t, int32(0), totalReceived.Load())
   446  	})
   447  
   448  	t.Run("age drop", func(t *testing.T) {
   449  		resourceURLNotifier := newTestResourceNotifier(time.Minute, 10*time.Millisecond, 500)
   450  		defer resourceURLNotifier.Stop(true)
   451  		totalReceived := atomic.Int32{}
   452  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   453  			time.Sleep(5 * time.Millisecond)
   454  			_, err := ReceiveWebhookEvent(r, authProvider)
   455  			require.NoError(t, err)
   456  			totalReceived.Inc()
   457  		}
   458  		// send multiple notifications
   459  		for i := 0; i < 10; i++ {
   460  			_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   461  			time.Sleep(time.Millisecond)
   462  			_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventParticipantJoined})
   463  			time.Sleep(time.Millisecond)
   464  			_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   465  			time.Sleep(time.Millisecond)
   466  		}
   467  
   468  		time.Sleep(2 * webhookCheckInterval)
   469  
   470  		// at least one request dropped
   471  		require.Greater(t, int32(30), totalReceived.Load())
   472  		require.Less(t, int32(0), totalReceived.Load())
   473  	})
   474  
   475  	t.Run("resource queue timeout", func(t *testing.T) {
   476  		resourceURLNotifier := newTestResourceNotifier(5*time.Millisecond, time.Minute, 500)
   477  		defer resourceURLNotifier.Stop(true)
   478  		totalReceived := atomic.Int32{}
   479  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   480  			_, err := ReceiveWebhookEvent(r, authProvider)
   481  			require.NoError(t, err)
   482  			totalReceived.Inc()
   483  		}
   484  
   485  		// check that resource queues change for the same event key
   486  		for i := 0; i < 3; i++ {
   487  			var rq *resourceQueue
   488  
   489  			roomName := fmt.Sprintf("room%d", i)
   490  
   491  			_ = resourceURLNotifier.QueueNotify(
   492  				context.Background(),
   493  				&livekit.WebhookEvent{
   494  					Event: EventRoomStarted,
   495  					Room: &livekit.Room{
   496  						Name: roomName,
   497  					},
   498  				},
   499  			)
   500  			resourceURLNotifier.mu.RLock()
   501  			rqi := resourceURLNotifier.resourceQueues[roomName]
   502  			resourceURLNotifier.mu.RUnlock()
   503  			require.NotNil(t, rqi)
   504  			require.NotNil(t, rqi.resourceQueue)
   505  			require.NotSame(t, rqi.resourceQueue, rq)
   506  			rq = rqi.resourceQueue
   507  			time.Sleep(10 * time.Millisecond)
   508  
   509  			_ = resourceURLNotifier.QueueNotify(
   510  				context.Background(),
   511  				&livekit.WebhookEvent{
   512  					Event: EventParticipantJoined,
   513  					Room: &livekit.Room{
   514  						Name: roomName,
   515  					},
   516  				},
   517  			)
   518  			resourceURLNotifier.mu.RLock()
   519  			rqi = resourceURLNotifier.resourceQueues[roomName]
   520  			resourceURLNotifier.mu.RUnlock()
   521  			require.NotNil(t, rqi)
   522  			require.NotNil(t, rqi.resourceQueue)
   523  			require.NotSame(t, rqi.resourceQueue, rq)
   524  			rq = rqi.resourceQueue
   525  			time.Sleep(10 * time.Millisecond)
   526  
   527  			_ = resourceURLNotifier.QueueNotify(
   528  				context.Background(),
   529  				&livekit.WebhookEvent{
   530  					Event: EventParticipantLeft,
   531  					Room: &livekit.Room{
   532  						Name: roomName,
   533  					},
   534  				},
   535  			)
   536  			resourceURLNotifier.mu.RLock()
   537  			rqi = resourceURLNotifier.resourceQueues[roomName]
   538  			resourceURLNotifier.mu.RUnlock()
   539  			require.NotNil(t, rqi)
   540  			require.NotNil(t, rqi.resourceQueue)
   541  			require.NotSame(t, rqi.resourceQueue, rq)
   542  			rq = rqi.resourceQueue
   543  			time.Sleep(10 * time.Millisecond)
   544  		}
   545  
   546  		time.Sleep(webhookCheckInterval)
   547  
   548  		require.Equal(t, int32(9), totalReceived.Load())
   549  	})
   550  }
   551  
   552  func TestResourceURLNotifierLifecycle(t *testing.T) {
   553  	s := newServer(testAddr)
   554  	require.NoError(t, s.Start())
   555  	defer s.Stop()
   556  
   557  	t.Run("start/stop without use", func(t *testing.T) {
   558  		resourceURLNotifier := newTestResourceNotifier(time.Minute, 200*time.Millisecond, 50)
   559  		resourceURLNotifier.Stop(false)
   560  	})
   561  
   562  	t.Run("sweeper", func(t *testing.T) {
   563  		resourceURLNotifier := newTestResourceNotifier(200*time.Millisecond, 200*time.Millisecond, 50)
   564  		numCalled := atomic.Int32{}
   565  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   566  			numCalled.Inc()
   567  		}
   568  		for i := 0; i < 10; i++ {
   569  			roomName := fmt.Sprintf("room%d", i)
   570  			_ = resourceURLNotifier.QueueNotify(
   571  				context.Background(),
   572  				&livekit.WebhookEvent{
   573  					Event: EventRoomStarted,
   574  					Room: &livekit.Room{
   575  						Name: roomName,
   576  					},
   577  				},
   578  			)
   579  
   580  			_ = resourceURLNotifier.QueueNotify(
   581  				context.Background(),
   582  				&livekit.WebhookEvent{
   583  					Event: EventRoomFinished,
   584  					Room: &livekit.Room{
   585  						Name: roomName,
   586  					},
   587  				},
   588  			)
   589  		}
   590  
   591  		resourceURLNotifier.mu.RLock()
   592  		require.Equal(t, 10, len(resourceURLNotifier.resourceQueues))
   593  		resourceURLNotifier.mu.RUnlock()
   594  
   595  		time.Sleep(time.Second)
   596  
   597  		// should have reaped after some time
   598  		resourceURLNotifier.mu.RLock()
   599  		require.Equal(t, 0, len(resourceURLNotifier.resourceQueues))
   600  		resourceURLNotifier.mu.RUnlock()
   601  
   602  		require.Equal(t, int32(20), numCalled.Load())
   603  	})
   604  
   605  	t.Run("stop allowing to drain", func(t *testing.T) {
   606  		resourceURLNotifier := newTestResourceNotifier(time.Minute, 200*time.Millisecond, 50)
   607  		numCalled := atomic.Int32{}
   608  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   609  			numCalled.Inc()
   610  		}
   611  		for i := 0; i < 10; i++ {
   612  			_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   613  			_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   614  		}
   615  		resourceURLNotifier.Stop(false)
   616  		require.Eventually(t, func() bool { return numCalled.Load() == 20 }, 5*time.Second, webhookCheckInterval)
   617  	})
   618  
   619  	t.Run("force stop", func(t *testing.T) {
   620  		resourceURLNotifier := newTestResourceNotifier(time.Minute, 200*time.Millisecond, 50)
   621  		numCalled := atomic.Int32{}
   622  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   623  			numCalled.Inc()
   624  		}
   625  		for i := 0; i < 10; i++ {
   626  			_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   627  			_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   628  		}
   629  		resourceURLNotifier.Stop(true)
   630  		time.Sleep(time.Second)
   631  		require.Greater(t, int32(20), numCalled.Load())
   632  	})
   633  
   634  	t.Run("times out after accepting connection", func(t *testing.T) {
   635  		params := ResourceURLNotifierParams{
   636  			URL:       testUrl,
   637  			APIKey:    testAPIKey,
   638  			APISecret: testAPISecret,
   639  			Config: ResourceURLNotifierConfig{
   640  				MaxAge:   200 * time.Millisecond,
   641  				MaxDepth: 50,
   642  			},
   643  			HTTPClientParams: HTTPClientParams{
   644  				RetryWaitMax:  time.Millisecond,
   645  				MaxRetries:    1,
   646  				ClientTimeout: 100 * time.Millisecond,
   647  			},
   648  		}
   649  
   650  		resourceURLNotifier := NewResourceURLNotifier(params)
   651  
   652  		numCalled := atomic.Int32{}
   653  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   654  			w.WriteHeader(200)
   655  			w.Write([]byte("ok"))
   656  
   657  			// delay the request to cause it to fail
   658  			time.Sleep(time.Second)
   659  			if r.Context().Err() == nil {
   660  				// inc if not canceled
   661  				numCalled.Inc()
   662  			}
   663  		}
   664  		defer resourceURLNotifier.Stop(false)
   665  
   666  		err := resourceURLNotifier.send(&livekit.WebhookEvent{Event: EventRoomStarted}, &params)
   667  		require.Error(t, err)
   668  	})
   669  
   670  	t.Run("times out before connection", func(t *testing.T) {
   671  		ln, err := net.Listen("tcp", ":9987")
   672  		require.NoError(t, err)
   673  		defer ln.Close()
   674  
   675  		params := ResourceURLNotifierParams{
   676  			URL:       "http://localhost:9987",
   677  			APIKey:    testAPIKey,
   678  			APISecret: testAPISecret,
   679  			Config: ResourceURLNotifierConfig{
   680  				MaxAge:   200 * time.Millisecond,
   681  				MaxDepth: 50,
   682  			},
   683  			HTTPClientParams: HTTPClientParams{
   684  				RetryWaitMax:  time.Millisecond,
   685  				MaxRetries:    1,
   686  				ClientTimeout: 100 * time.Millisecond,
   687  			},
   688  		}
   689  
   690  		resourceURLNotifier := NewResourceURLNotifier(params)
   691  		defer resourceURLNotifier.Stop(false)
   692  
   693  		startedAt := time.Now()
   694  		err = resourceURLNotifier.send(&livekit.WebhookEvent{Event: EventRoomStarted}, &params)
   695  		require.Error(t, err)
   696  		require.Less(t, time.Since(startedAt).Seconds(), float64(2))
   697  	})
   698  }
   699  
   700  func TestResourceURLNotifierFilter(t *testing.T) {
   701  	s := newServer(testAddr)
   702  	require.NoError(t, s.Start())
   703  	defer s.Stop()
   704  
   705  	t.Run("none", func(t *testing.T) {
   706  		resourceURLNotifier := NewResourceURLNotifier(ResourceURLNotifierParams{
   707  			URL:       testUrl,
   708  			APIKey:    testAPIKey,
   709  			APISecret: testAPISecret,
   710  			Config: ResourceURLNotifierConfig{
   711  				MaxAge:   200 * time.Millisecond,
   712  				MaxDepth: 50,
   713  			},
   714  			FilterParams: FilterParams{},
   715  		})
   716  		defer resourceURLNotifier.Stop(false)
   717  
   718  		numCalled := atomic.Int32{}
   719  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   720  			numCalled.Inc()
   721  		}
   722  
   723  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   724  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   725  		require.Eventually(
   726  			t,
   727  			func() bool {
   728  				return numCalled.Load() == 2
   729  			},
   730  			5*time.Second,
   731  			webhookCheckInterval,
   732  		)
   733  	})
   734  
   735  	t.Run("includes", func(t *testing.T) {
   736  		resourceURLNotifier := NewResourceURLNotifier(ResourceURLNotifierParams{
   737  			URL:       testUrl,
   738  			APIKey:    testAPIKey,
   739  			APISecret: testAPISecret,
   740  			Config: ResourceURLNotifierConfig{
   741  				MaxAge:   200 * time.Millisecond,
   742  				MaxDepth: 50,
   743  			},
   744  			FilterParams: FilterParams{
   745  				IncludeEvents: []string{EventRoomStarted},
   746  			},
   747  		})
   748  		defer resourceURLNotifier.Stop(false)
   749  
   750  		numCalled := atomic.Int32{}
   751  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   752  			numCalled.Inc()
   753  		}
   754  
   755  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   756  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   757  		require.Eventually(
   758  			t,
   759  			func() bool {
   760  				return numCalled.Load() == 1
   761  			},
   762  			5*time.Second,
   763  			webhookCheckInterval,
   764  		)
   765  	})
   766  
   767  	t.Run("excludes", func(t *testing.T) {
   768  		resourceURLNotifier := NewResourceURLNotifier(ResourceURLNotifierParams{
   769  			URL:       testUrl,
   770  			APIKey:    testAPIKey,
   771  			APISecret: testAPISecret,
   772  			Config: ResourceURLNotifierConfig{
   773  				MaxAge:   200 * time.Millisecond,
   774  				MaxDepth: 50,
   775  			},
   776  			FilterParams: FilterParams{
   777  				ExcludeEvents: []string{EventRoomStarted},
   778  			},
   779  		})
   780  		defer resourceURLNotifier.Stop(false)
   781  
   782  		numCalled := atomic.Int32{}
   783  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   784  			numCalled.Inc()
   785  		}
   786  
   787  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   788  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   789  		require.Eventually(
   790  			t,
   791  			func() bool {
   792  				return numCalled.Load() == 1
   793  			},
   794  			5*time.Second,
   795  			webhookCheckInterval,
   796  		)
   797  	})
   798  
   799  	t.Run("includes + excludes", func(t *testing.T) {
   800  		resourceURLNotifier := NewResourceURLNotifier(ResourceURLNotifierParams{
   801  			URL:       testUrl,
   802  			APIKey:    testAPIKey,
   803  			APISecret: testAPISecret,
   804  			Config: ResourceURLNotifierConfig{
   805  				MaxAge:   200 * time.Millisecond,
   806  				MaxDepth: 50,
   807  			},
   808  			FilterParams: FilterParams{
   809  				IncludeEvents: []string{EventRoomStarted},
   810  				ExcludeEvents: []string{EventRoomStarted, EventRoomFinished},
   811  			},
   812  		})
   813  		defer resourceURLNotifier.Stop(false)
   814  
   815  		numCalled := atomic.Int32{}
   816  		s.handler = func(w http.ResponseWriter, r *http.Request) {
   817  			numCalled.Inc()
   818  		}
   819  
   820  		// EventRoomStarted should be allowed as IncludeEvents take precedence
   821  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomStarted})
   822  		_ = resourceURLNotifier.QueueNotify(context.Background(), &livekit.WebhookEvent{Event: EventRoomFinished})
   823  		require.Eventually(
   824  			t,
   825  			func() bool {
   826  				return numCalled.Load() == 1
   827  			},
   828  			5*time.Second,
   829  			webhookCheckInterval,
   830  		)
   831  	})
   832  }
   833  
   834  func newTestResourceNotifier(timeout time.Duration, maxAge time.Duration, maxDepth int) *ResourceURLNotifier {
   835  	return NewResourceURLNotifier(ResourceURLNotifierParams{
   836  		URL:       testUrl,
   837  		APIKey:    testAPIKey,
   838  		APISecret: testAPISecret,
   839  		Timeout:   timeout,
   840  		Config: ResourceURLNotifierConfig{
   841  			MaxAge:   maxAge,
   842  			MaxDepth: maxDepth,
   843  		},
   844  	})
   845  }
   846  
   847  // ---------------------------------------
   848  
   849  type testServer struct {
   850  	handler func(w http.ResponseWriter, r *http.Request)
   851  	server  *http.Server
   852  }
   853  
   854  func newServer(addr string) *testServer {
   855  	s := &testServer{}
   856  	s.server = &http.Server{
   857  		Addr:    addr,
   858  		Handler: s,
   859  	}
   860  	return s
   861  }
   862  
   863  func (s *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   864  	if s.handler != nil {
   865  		s.handler(w, r)
   866  	}
   867  }
   868  
   869  func (s *testServer) Start() error {
   870  	l, err := net.Listen("tcp", s.server.Addr)
   871  	if err != nil {
   872  		return err
   873  	}
   874  	go s.server.Serve(l)
   875  	return nil
   876  }
   877  
   878  func (s *testServer) Stop() {
   879  	_ = s.server.Shutdown(context.Background())
   880  }