Ctrl K
ComunidadGuías…

server/internal/hub/hub_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package hub_test

import (
	"testing"
	"time"

	"blackbox/server/internal/hub"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestHub_BroadcastReachesSubscribers(t *testing.T) {
	h := hub.New()

	_, ch1, _, unsub1, err := h.Subscribe("u1", "10.0.0.1")
	require.NoError(t, err)
	_, ch2, _, unsub2, err := h.Subscribe("u2", "10.0.0.2")
	require.NoError(t, err)
	defer unsub1()
	defer unsub2()

	h.Broadcast([]byte(`{"type":"test"}`))

	select {
	case msg := <-ch1:
		assert.Equal(t, `{"type":"test"}`, string(msg))
	case <-time.After(100 * time.Millisecond):
		t.Fatal("ch1 did not receive message")
	}

	select {
	case msg := <-ch2:
		assert.Equal(t, `{"type":"test"}`, string(msg))
	case <-time.After(100 * time.Millisecond):
		t.Fatal("ch2 did not receive message")
	}
}

func TestHub_UnsubscribedClientReceivesNothing(t *testing.T) {
	h := hub.New()

	_, ch, _, unsub, err := h.Subscribe("u1", "10.0.0.1")
	require.NoError(t, err)
	unsub()

	h.Broadcast([]byte(`{"type":"test"}`))

	select {
	case msg, ok := <-ch:
		if ok {
			t.Fatalf("unsubscribed channel received unexpected message: %s", msg)
		}
		// channel was closed by unsub — that's expected, not a broadcast
	case <-time.After(50 * time.Millisecond):
		// also correct: close signal not yet observed
	}
}

func TestHub_SlowClientDoesNotBlockBroadcast(t *testing.T) {
	h := hub.New()

	// Subscribe with a channel that fills immediately (buffer size 32)
	_, _, _, unsub, err := h.Subscribe("u1", "10.0.0.1")
	require.NoError(t, err)
	defer unsub()

	// Fill that channel by broadcasting 33 messages - should not deadlock
	done := make(chan struct{})
	go func() {
		for i := 0; i < 33; i++ {
			h.Broadcast([]byte(`{}`))
		}
		close(done)
	}()

	select {
	case <-done:
		// correct
	case <-time.After(500 * time.Millisecond):
		t.Fatal("Broadcast blocked on slow client")
	}
}

func TestHub_UnsubscribeIsIdempotent(t *testing.T) {
	h := hub.New()

	_, _, _, unsub, err := h.Subscribe("u1", "10.0.0.1")
	require.NoError(t, err)
	unsub()
	unsub()
}

func TestHub_InvalidateUserDisconnectsMatchingClients(t *testing.T) {
	h := hub.New()

	_, _, disconnect1, unsub1, err := h.Subscribe("u1", "10.0.0.1")
	require.NoError(t, err)
	defer unsub1()

	_, ch2, disconnect2, unsub2, err := h.Subscribe("u2", "10.0.0.2")
	require.NoError(t, err)
	defer unsub2()

	h.InvalidateUser("u1")

	select {
	case reason := <-disconnect1:
		assert.Equal(t, "session invalidated", reason)
	case <-time.After(100 * time.Millisecond):
		t.Fatal("invalidated client did not receive disconnect signal")
	}

	select {
	case <-disconnect2:
		t.Fatal("non-matching client should not be disconnected")
	case <-time.After(50 * time.Millisecond):
	}

	h.Broadcast([]byte(`{"type":"test"}`))

	select {
	case msg := <-ch2:
		assert.Equal(t, `{"type":"test"}`, string(msg))
	case <-time.After(100 * time.Millisecond):
		t.Fatal("remaining client did not receive message")
	}
}

func TestHub_SubscribeEnforcesPerUserLimit(t *testing.T) {
	h := hub.New()

	unsubs := make([]func(), 0, 5)
	for i := 0; i < 5; i++ {
		_, _, _, unsub, err := h.Subscribe("u1", "10.0.0.1")
		require.NoError(t, err)
		unsubs = append(unsubs, unsub)
	}
	defer func() {
		for _, unsub := range unsubs {
			unsub()
		}
	}()

	_, _, _, _, err := h.Subscribe("u1", "10.0.0.2")
	require.ErrorIs(t, err, hub.ErrTooManyUserConnections)
}

func TestHub_SubscribeEnforcesPerIPLimit(t *testing.T) {
	h := hub.New()

	unsubs := make([]func(), 0, 20)
	for i := 0; i < 20; i++ {
		userID := time.Now().Add(time.Duration(i) * time.Nanosecond).Format(time.RFC3339Nano)
		_, _, _, unsub, err := h.Subscribe(userID, "10.0.0.1")
		require.NoError(t, err)
		unsubs = append(unsubs, unsub)
	}
	defer func() {
		for _, unsub := range unsubs {
			unsub()
		}
	}()

	_, _, _, _, err := h.Subscribe("overflow", "10.0.0.1")
	require.ErrorIs(t, err, hub.ErrTooManyIPConnections)
}