From dd3f7d58b543d1016d84e3d0478753d2a6afd7fe Mon Sep 17 00:00:00 2001 From: henrygd Date: Sat, 19 Sep 2026 00:10:02 -0400 Subject: [PATCH] test: use virtual time to eliminate MQTT notification timeout waits --- internal/alerts/alerts_api_test.go | 15 +++++ internal/alerts/notification_client_test.go | 65 ++++++++++++--------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/internal/alerts/alerts_api_test.go b/internal/alerts/alerts_api_test.go index a8f2748c..44a931d7 100644 --- a/internal/alerts/alerts_api_test.go +++ b/internal/alerts/alerts_api_test.go @@ -11,6 +11,7 @@ import ( "strings" "sync/atomic" "testing" + "testing/synctest" beszelTests "github.com/henrygd/beszel/internal/tests" pbTests "github.com/pocketbase/pocketbase/tests" @@ -533,6 +534,20 @@ func TestSendTestNotification(t *testing.T) { for _, url := range []string{localURL, "smtp://user:pass@127.0.0.1/?fromAddress=sender@example.com&toAddresses=recipient@example.com", "mqtt://127.0.0.1/topic"} { scenarios = append(scenarios, beszelTests.ApiScenario{ + BeforeTestFunc: func(tb testing.TB, _ *pbTests.TestApp, e *core.ServeEvent) { + if !strings.HasPrefix(url, "mqtt://") { + return + } + // Keep the real MQTT rejection path, but advance its library's + // fixed timeout using virtual time instead of waiting 10 seconds. + e.Router.BindFunc(func(re *core.RequestEvent) error { + var err error + synctest.Test(tb.(*testing.T), func(t *testing.T) { + err = re.Next() + }) + return err + }) + }, Name: "readonly cannot send to " + url, Method: http.MethodPost, URL: "/api/beszel/test-notification", diff --git a/internal/alerts/notification_client_test.go b/internal/alerts/notification_client_test.go index cc564898..77b6bbae 100644 --- a/internal/alerts/notification_client_test.go +++ b/internal/alerts/notification_client_test.go @@ -11,6 +11,7 @@ import ( "strings" "sync/atomic" "testing" + "testing/synctest" "github.com/nicholas-fedor/shoutrrr/pkg/types" "golang.org/x/net/dns/dnsmessage" @@ -178,39 +179,45 @@ func TestPublicNotificationTCP(t *testing.T) { } { t.Run(rawURL, func(t *testing.T) { t.Parallel() + // MQTT waits for a fixed library timeout even after a dial failure. + // Virtual time preserves the full send/cleanup path without that delay. t.Run("internal destination", func(t *testing.T) { - err := sendPublicNotification(strings.ReplaceAll(rawURL, "HOST", "127.0.0.1"), "test") - if !errors.Is(err, errInternalDestination) { - t.Fatalf("expected blocked destination, got %v", err) - } + synctest.Test(t, func(t *testing.T) { + err := sendPublicNotification(strings.ReplaceAll(rawURL, "HOST", "127.0.0.1"), "test") + if !errors.Is(err, errInternalDestination) { + t.Fatalf("expected blocked destination, got %v", err) + } + }) }) t.Run("public destination uses injected dialer", func(t *testing.T) { - var calls atomic.Int32 - stopped := errors.New("test dial stopped") - service, err := newPublicNotificationService(strings.ReplaceAll(rawURL, "HOST", "8.8.8.8"), types.SenderOptions{ - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - calls.Add(1) - if network != "tcp" || !strings.HasPrefix(address, "8.8.8.8:") { - t.Errorf("unexpected dial: %s %s", network, address) - } - if err := checkNotificationAddress(address); err != nil { - t.Error(err) - } - return nil, stopped - }, + synctest.Test(t, func(t *testing.T) { + var calls atomic.Int32 + stopped := errors.New("test dial stopped") + service, err := newPublicNotificationService(strings.ReplaceAll(rawURL, "HOST", "8.8.8.8"), types.SenderOptions{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + calls.Add(1) + if network != "tcp" || !strings.HasPrefix(address, "8.8.8.8:") { + t.Errorf("unexpected dial: %s %s", network, address) + } + if err := checkNotificationAddress(address); err != nil { + t.Error(err) + } + return nil, stopped + }, + }) + if err != nil { + t.Fatal(err) + } + if closer, ok := service.(io.Closer); ok { + defer closer.Close() + } + if err := service.Send("test", &types.Params{}); err == nil { + t.Fatal("expected dial failure") + } + if calls.Load() == 0 { + t.Fatal("custom dialer was not used") + } }) - if err != nil { - t.Fatal(err) - } - if closer, ok := service.(io.Closer); ok { - defer closer.Close() - } - if err := service.Send("test", &types.Params{}); err == nil { - t.Fatal("expected dial failure") - } - if calls.Load() == 0 { - t.Fatal("custom dialer was not used") - } }) }) }