package notify import ( "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestClose_Basic verifies basic Close() functionality. func TestClose_Basic(t *testing.T) { n := NewNotifier(50) // Create some subscribers sub1, err := n.Subscribe() require.NoError(t, err) sub2, err := n.Subscribe() require.NoError(t, err) sub3, err := n.Subscribe() require.NoError(t, err) assert.Equal(t, 3, len(n.subscribers), "Should have 3 subscribers") // Close the notifier n.Close() // Verify all subscribers removed assert.Equal(t, 0, len(n.subscribers), "Should have 0 subscribers after close") // Verify channels are closed _, ok := <-sub1.Listen() assert.False(t, ok, "sub1 channel should be closed") _, ok = <-sub2.Listen() assert.False(t, ok, "sub2 channel should be closed") _, ok = <-sub3.Listen() assert.False(t, ok, "sub3 channel should be closed") } // TestClose_IdempotentClose verifies that calling Close() multiple times is safe. func TestClose_IdempotentClose(t *testing.T) { n := NewNotifier(50) sub, err := n.Subscribe() require.NoError(t, err) // Close multiple times - should not panic assert.NotPanics(t, func() { n.Close() n.Close() n.Close() }, "Multiple Close() calls should not panic") // Verify channel is still closed (not double-closed) _, ok := <-sub.Listen() assert.False(t, ok, "Channel should be closed") } // TestClose_SubscribeAfterClose verifies that Subscribe fails after Close. func TestClose_SubscribeAfterClose(t *testing.T) { n := NewNotifier(50) // Subscribe before close sub1, err := n.Subscribe() require.NoError(t, err) require.NotNil(t, sub1) // Close n.Close() // Try to subscribe after close sub2, err := n.Subscribe() assert.Error(t, err, "Subscribe should return error after Close") assert.Nil(t, sub2, "Subscribe should return nil subscriber after Close") assert.Contains(t, err.Error(), "closed", "Error should mention notifier is closed") } // TestClose_NotifyAfterClose verifies that Notify after Close doesn't panic. func TestClose_NotifyAfterClose(t *testing.T) { n := NewNotifier(50) sub, err := n.Subscribe() require.NoError(t, err) // Close n.Close() // Try to notify - should not panic notification := Notification{ Target: sub.ID, Level: LevelInfo, Message: "Should be ignored", } assert.NotPanics(t, func() { n.Notify(notification) }, "Notify after Close should not panic") } // TestClose_NotifyAllAfterClose verifies that NotifyAll after Close doesn't panic. func TestClose_NotifyAllAfterClose(t *testing.T) { n := NewNotifier(50) _, err := n.Subscribe() require.NoError(t, err) // Close n.Close() // Try to broadcast - should not panic notification := Notification{ Level: LevelInfo, Message: "Should be ignored", } assert.NotPanics(t, func() { n.NotifyAll(notification) }, "NotifyAll after Close should not panic") } // TestClose_WithActiveListeners verifies that listeners detect channel closure. func TestClose_WithActiveListeners(t *testing.T) { n := NewNotifier(50) sub, err := n.Subscribe() require.NoError(t, err) var wg sync.WaitGroup listenerExited := false // Start listener goroutine wg.Add(1) go func() { defer wg.Done() for range sub.Listen() { // Process notifications } listenerExited = true }() // Give listener time to start time.Sleep(10 * time.Millisecond) // Close notifier n.Close() // Wait for listener to exit done := make(chan bool) go func() { wg.Wait() done <- true }() select { case <-done: assert.True(t, listenerExited, "Listener should have exited") case <-time.After(1 * time.Second): t.Fatal("Listener did not exit after Close - possible hang") } } // TestClose_PendingNotifications verifies behavior of pending notifications on close. func TestClose_PendingNotifications(t *testing.T) { n := NewNotifier(50) sub, err := n.Subscribe() require.NoError(t, err) // Send some notifications for i := 0; i < 10; i++ { notification := Notification{ Target: sub.ID, Level: LevelInfo, Message: "Notification", } go n.Notify(notification) } // Wait for sends to complete time.Sleep(50 * time.Millisecond) // Close notifier (closes channels) n.Close() // Try to read any remaining notifications before closure received := 0 for { _, ok := <-sub.Listen() if !ok { break } received++ } t.Logf("Received %d notifications before channel closed", received) assert.GreaterOrEqual(t, received, 0, "Should receive at least 0 notifications") } // TestClose_ConcurrentSubscribeAndClose verifies thread safety. func TestClose_ConcurrentSubscribeAndClose(t *testing.T) { n := NewNotifier(50) var wg sync.WaitGroup // Goroutines trying to subscribe for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() _, _ = n.Subscribe() // May succeed or fail depending on timing }() } // Give some time for subscriptions to start time.Sleep(5 * time.Millisecond) // Close concurrently wg.Add(1) go func() { defer wg.Done() n.Close() }() // Should complete without deadlock or panic done := make(chan bool) go func() { wg.Wait() done <- true }() select { case <-done: // Success case <-time.After(2 * time.Second): t.Fatal("Test timed out - possible deadlock") } // After close, no more subscriptions should succeed sub, err := n.Subscribe() assert.Error(t, err) assert.Nil(t, sub) } // TestClose_ConcurrentNotifyAndClose verifies thread safety with notifications. func TestClose_ConcurrentNotifyAndClose(t *testing.T) { n := NewNotifier(50) // Create some subscribers subscribers := make([]*Subscriber, 10) for i := 0; i < 10; i++ { sub, err := n.Subscribe() require.NoError(t, err) subscribers[i] = sub } var wg sync.WaitGroup // Goroutines sending notifications for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() notification := Notification{ Level: LevelInfo, Message: "Test", } n.NotifyAll(notification) }() } // Close concurrently time.Sleep(5 * time.Millisecond) wg.Add(1) go func() { defer wg.Done() n.Close() }() // Should complete without panic or deadlock done := make(chan bool) go func() { wg.Wait() done <- true }() select { case <-done: // Success - no panic or deadlock case <-time.After(2 * time.Second): t.Fatal("Test timed out - possible deadlock") } } // TestClose_Integration verifies the complete Close workflow. func TestClose_Integration(t *testing.T) { n := NewNotifier(50) // Create subscribers sub1, err := n.Subscribe() require.NoError(t, err) sub2, err := n.Subscribe() require.NoError(t, err) sub3, err := n.Subscribe() require.NoError(t, err) // Send some notifications notification := Notification{ Level: LevelSuccess, Message: "Before close", } go n.NotifyAll(notification) // Receive notifications from all subscribers received1, ok := receiveWithTimeout(sub1.Listen(), 100*time.Millisecond) require.True(t, ok, "sub1 should receive notification") assert.Equal(t, "Before close", received1.Message) received2, ok := receiveWithTimeout(sub2.Listen(), 100*time.Millisecond) require.True(t, ok, "sub2 should receive notification") assert.Equal(t, "Before close", received2.Message) received3, ok := receiveWithTimeout(sub3.Listen(), 100*time.Millisecond) require.True(t, ok, "sub3 should receive notification") assert.Equal(t, "Before close", received3.Message) // Close the notifier n.Close() // Verify all channels closed (should return immediately with ok=false) _, ok = <-sub1.Listen() assert.False(t, ok, "sub1 should be closed") _, ok = <-sub2.Listen() assert.False(t, ok, "sub2 should be closed") _, ok = <-sub3.Listen() assert.False(t, ok, "sub3 should be closed") // Verify no more subscriptions sub4, err := n.Subscribe() assert.Error(t, err) assert.Nil(t, sub4) // Verify notifications are ignored notification2 := Notification{ Level: LevelInfo, Message: "After close", } assert.NotPanics(t, func() { n.NotifyAll(notification2) }) } // TestClose_UnsubscribeAfterClose verifies unsubscribe after close is safe. func TestClose_UnsubscribeAfterClose(t *testing.T) { n := NewNotifier(50) sub, err := n.Subscribe() require.NoError(t, err) // Close notifier n.Close() // Try to unsubscribe after close - should be safe assert.NotPanics(t, func() { sub.Unsubscribe() }, "Unsubscribe after Close should not panic") }