0% found this document useful (0 votes)
17 views12 pages

See Test

The document contains a Go package for testing a Server-Sent Events (SSE) client, including a mock database and client structure. It features various test cases for client registration, event processing, reconnection logic, and timeout behavior. The tests utilize the testify library for assertions and mock functionalities to validate the expected outcomes of the SSE client operations.

Uploaded by

cosegad183
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
17 views12 pages

See Test

The document contains a Go package for testing a Server-Sent Events (SSE) client, including a mock database and client structure. It features various test cases for client registration, event processing, reconnection logic, and timeout behavior. The tests utilize the testify library for assertions and mock functionalities to validate the expected outcomes of the SSE client operations.

Uploaded by

cosegad183
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 12

package sse

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

// MockDatabase implements a minimal database interface for testing


type MockDatabase struct {
mock.Mock
}

func (m *MockDatabase) CreateSSEMessageLog(event map[string]interface{}, chatID,


from, to string) (*struct {
ID uuid.UUID
}, error) {
args := m.Called(event, chatID, from, to)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return &struct {
ID uuid.UUID
}{ID: args.Get(0).(uuid.UUID)}, args.Error(1)
}

// TestClient is a simplified version of Client for testing


type TestClient struct {
URL string
ChatID string
WebhookURL string
LastEventID string
RetryInterval time.Duration
Client *http.Client
DB *MockDatabase
stopChan chan struct{}
firstFailTime time.Time
}

// NewTestClient creates a test client


func NewTestClient(sseURL string, chatID string, webhookURL string, db
*MockDatabase) *TestClient {
return &TestClient{
URL: sseURL,
ChatID: chatID,
WebhookURL: webhookURL,
RetryInterval: 3 * time.Second,
Client: &http.Client{
Timeout: 0,
},
DB: db,
stopChan: make(chan struct{}),
}
}

// Start begins the connection process in a goroutine (test version)


func (c *TestClient) Start() {
// For testing, we just register the client
ClientRegistry.Register(&Client{
URL: c.URL,
ChatID: c.ChatID,
WebhookURL: c.WebhookURL,
stopChan: make(chan struct{}),
})
}

// Stop terminates the test client


func (c *TestClient) Stop() {
close(c.stopChan)
// Also unregister from registry
ClientRegistry.Unregister(c.URL, c.ChatID)
}

// storeEvent processes and stores an SSE event (test version)


func (c *TestClient) storeEvent(eventData map[string]string) error {
var parsedEvent map[string]interface{}

// First try to parse as JSON if it looks like JSON


if strings.TrimSpace(eventData["data"])[0] == '{' {
err := json.Unmarshal([]byte(eventData["data"]), &parsedEvent)
if err != nil {
// If not valid JSON, store as string
parsedEvent = map[string]interface{}{
"raw": eventData["data"],
}
}
} else {
// Store as raw data
parsedEvent = map[string]interface{}{
"raw": eventData["data"],
}
}

// Add SSE metadata


if eventData["id"] != "" {
parsedEvent["id"] = eventData["id"]
}
if eventData["event"] != "" {
parsedEvent["event_type"] = eventData["event"]
}

// Call the mock database


_, err := c.DB.CreateSSEMessageLog(parsedEvent, c.ChatID, c.URL,
c.WebhookURL)
return err
}

func TestGenerateClientKey(t *testing.T) {


tests := []struct {
name string
sseURL string
chatID string
expected string
}{
{
name: "Standard Input",
sseURL: "https://example.com/events",
chatID: "chat-123",
expected: "chat-123:https://example.com/events",
},
{
name: "Empty URL",
sseURL: "",
chatID: "chat-123",
expected: "chat-123:",
},
{
name: "Empty ChatID",
sseURL: "https://example.com/events",
chatID: "",
expected: ":https://example.com/events",
},
{
name: "Both Empty",
sseURL: "",
chatID: "",
expected: ":",
},
{
name: "Special Characters",
sseURL: "https://example.com/events?param=value&other=123",
chatID: "chat-123!@#",
expected: "chat-123!@#:https://example.com/events?
param=value&other=123",
},
{
name: "Very Long Strings",
sseURL: strings.Repeat("a", 1000),
chatID: strings.Repeat("b", 1000),
expected: strings.Repeat("b", 1000) + ":" + strings.Repeat("a",
1000),
},
}

for _, tt := range tests {


t.Run(tt.name, func(t *testing.T) {
result := GenerateClientKey(tt.sseURL, tt.chatID)
assert.Equal(t, tt.expected, result)
})
}
}

func TestRegistry_RegisterAndUnregister(t *testing.T) {


// Create a new registry for testing
registry := &Registry{
clients: make(map[string]*Client),
mutex: &sync.RWMutex{},
}
// Create a client for testing
client := &Client{
URL: "https://example.com/events",
ChatID: "chat-123",
WebhookURL: "https://webhook.com",
stopChan: make(chan struct{}),
}

// Test Register
registry.Register(client)

// The key in the Register implementation is generated as chatID:URL


key := fmt.Sprintf("%s:%s", client.ChatID, client.URL)

// Check if client was registered


registry.mutex.RLock()
registeredClient, exists := registry.clients[key]
registry.mutex.RUnlock()

// Debug output to see what's happening


t.Logf("Key used for lookup: %s", key)
t.Logf("Registry contents: %v", registry.clients)

assert.True(t, exists, "Client should exist in registry")


assert.Equal(t, client, registeredClient, "Registered client should match
original client")

// Test Unregister - note the order of parameters


// The bug in the implementation is that Unregister swaps the parameters
// when calling GenerateClientKey
success := registry.Unregister(client.URL, client.ChatID)

// For debugging
unregisterKey := fmt.Sprintf("%s:%s", client.ChatID, client.URL) // Same as
key
t.Logf("Key used for unregister: %s", unregisterKey)

// Direct check of registry to see what's happening


registry.mutex.RLock()
for k := range registry.clients {
t.Logf("Remaining key in registry: %s", k)
}
registry.mutex.RUnlock()

assert.True(t, success, "Unregister should return true for existing client")

// Verify client was removed


registry.mutex.RLock()
_, exists = registry.clients[key]
registry.mutex.RUnlock()

assert.False(t, exists, "Client should be removed from registry")

// Test Unregister non-existent client


success = registry.Unregister("nonexistent", "nonexistent")
assert.False(t, success, "Unregister should return false for non-existent
client")
}
func TestClient_ProcessEvents(t *testing.T) {
tests := []struct {
name string
eventStream string
expectedEvents []map[string]string
expectError bool
}{
{
name: "Standard Event",
eventStream: "id: 1\nevent: update\ndata: {\"key\":\"value\"}\n\
n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "update",
"data": "{\"key\":\"value\"}",
},
},
expectError: false,
},
{
name: "Multiple Events",
eventStream: "id: 1\nevent: update\ndata: {\"key\":\"value1\"}\n\
n" +
"id: 2\nevent: update\ndata: {\"key\":\"value2\"}\n\n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "update",
"data": "{\"key\":\"value1\"}",
},
{
"id": "2",
"event": "update",
"data": "{\"key\":\"value2\"}",
},
},
expectError: false,
},
{
name: "Event Without ID",
eventStream: "event: update\ndata: {\"key\":\"value\"}\n\n",
expectedEvents: []map[string]string{
{
"id": "",
"event": "update",
"data": "{\"key\":\"value\"}",
},
},
expectError: false,
},
{
name: "Event Without Event Type",
eventStream: "id: 1\ndata: {\"key\":\"value\"}\n\n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "",
"data": "{\"key\":\"value\"}",
},
},
expectError: false,
},
{
name: "Multi-line Data",
eventStream: "id: 1\nevent: update\ndata: line1\ndata: line2\n\
n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "update",
"data": "line1\nline2",
},
},
expectError: false,
},
{
name: "Event With Retry",
eventStream: "id: 1\nevent: update\ndata: {\"key\":\"value\"}\
nretry: 5000\n\n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "update",
"data": "{\"key\":\"value\"}",
},
},
expectError: false,
},
{
name: "Event With Comment",
eventStream: ": this is a comment\nid: 1\nevent: update\ndata:
{\"key\":\"value\"}\n\n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "update",
"data": "{\"key\":\"value\"}",
},
},
expectError: false,
},
{
name: "Empty Event",
eventStream: "\n\n",
expectedEvents: []map[string]string{},
expectError: false,
},
{
name: "Malformed JSON Data",
eventStream: "id: 1\nevent: update\ndata: {\"key\":\"value\"\n\
n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "update",
"data": "{\"key\":\"value\"",
},
},
expectError: false,
},
{
name: "Leading Space in Data",
eventStream: "id: 1\nevent: update\ndata: {\"key\":\"value\"}\n\
n",
expectedEvents: []map[string]string{
{
"id": "1",
"event": "update",
"data": "{\"key\":\"value\"}",
},
},
expectError: false,
},
}

for _, tt := range tests {


t.Run(tt.name, func(t *testing.T) {
mockDB := new(MockDatabase)

// Set up expectations for each expected event


for _, event := range tt.expectedEvents {
eventData := event["data"]
var parsedEvent map[string]interface{}

if len(eventData) > 0 && eventData[0] == '{' {


err := json.Unmarshal([]byte(eventData),
&parsedEvent)
if err != nil {
parsedEvent = map[string]interface{}{
"raw": eventData,
}
}
} else {
parsedEvent = map[string]interface{}{
"raw": eventData,
}
}

if event["id"] != "" {
parsedEvent["id"] = event["id"]
}
if event["event"] != "" {
parsedEvent["event_type"] = event["event"]
}

mockID := uuid.New()
mockDB.On("CreateSSEMessageLog", mock.MatchedBy(func(e
map[string]interface{}) bool {
// Simple matching for test purposes
return true
}), "chat-123", "https://example.com/events",
"https://webhook.com").Return(mockID, nil)
}

client := NewTestClient("https://example.com/events", "chat-123",


"https://webhook.com", mockDB)

// Create a test server that returns the event stream


server := httptest.NewServer(http.HandlerFunc(func(w
http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, tt.eventStream)
}))
defer server.Close()

// Update client URL to point to test server


client.URL = server.URL

// Start client in a goroutine


go func() {
client.Start()
time.Sleep(100 * time.Millisecond)
client.Stop()
}()

// Allow time for events to be processed


time.Sleep(200 * time.Millisecond)

// Verify expectations
mockDB.AssertExpectations(t)
})
}
}

func TestClient_Reconnection(t *testing.T) {


mockDB := new(MockDatabase)
client := NewTestClient("https://example.com/events", "chat-123",
"https://webhook.com", mockDB)

// Set shorter retry interval for test


client.RetryInterval = 100 * time.Millisecond

// Track connection attempts


connectionCount := 0

// Create a server that disconnects after sending one event


server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r
*http.Request) {
connectionCount++

// Check for Last-Event-ID on reconnection


if connectionCount > 1 {
lastEventID := r.Header.Get("Last-Event-ID")
if connectionCount == 2 {
assert.Equal(t, "", lastEventID, "Expected no Last-Event-ID
on first reconnection")
} else if connectionCount == 3 {
assert.Equal(t, "last-event", lastEventID, "Expected Last-
Event-ID header to be set on second reconnection")
}
}

w.Header().Set("Content-Type", "text/event-stream")
w.WriteHeader(http.StatusOK)

// Send one event


if connectionCount == 2 {
fmt.Fprint(w, "id: last-event\ndata: test data\n\n")

// Set up mock for this event


mockID := uuid.New()
mockDB.On("CreateSSEMessageLog", mock.Anything, "chat-123",
mock.Anything, "https://webhook.com").Return(mockID, nil).Once()
}

// Force disconnect
hj, ok := w.(http.Hijacker)
if !ok {
t.Fatal("webserver doesn't support hijacking")
}
conn, _, err := hj.Hijack()
if err != nil {
t.Fatal(err)
}
conn.Close()
}))
defer server.Close()

// Update client URL to point to test server


client.URL = server.URL

// Start client
client.Start()

// Let it run for enough time to reconnect multiple times


time.Sleep(500 * time.Millisecond)

// Stop client
client.Stop()

// Verify reconnection happened


assert.GreaterOrEqual(t, connectionCount, 3, "Expected at least 3
connections")

// Verify expectations
mockDB.AssertExpectations(t)
}

func TestClient_TimeoutAfter60Minutes(t *testing.T) {


mockDB := new(MockDatabase)
client := NewTestClient("https://nonexistent.example.com", "chat-123",
"https://webhook.com", mockDB)

// Set shorter retry interval for test


client.RetryInterval = 10 * time.Millisecond

// Start client
client.Start()

// Set firstFailTime to just over 60 minutes ago


client.firstFailTime = time.Now().Add(-61 * time.Minute)
// Wait for client to stop itself
time.Sleep(100 * time.Millisecond)

// Try to send message to stopChan - should be closed


select {
case client.stopChan <- struct{}{}:
t.Error("Client did not stop after 60 minutes of failure")
default:
// Channel is closed or full, which is expected
}

// Verify client is unregistered


key := GenerateClientKey(client.URL, client.ChatID)
ClientRegistry.mutex.RLock()
_, exists := ClientRegistry.clients[key]
ClientRegistry.mutex.RUnlock()

assert.False(t, exists, "Client should be unregistered after timeout")


}

func TestClientRegistry_GlobalInstance(t *testing.T) {


// Clear the registry
ClientRegistry.mutex.Lock()
ClientRegistry.clients = make(map[string]*Client)
ClientRegistry.mutex.Unlock()

mockDB := new(MockDatabase)

// Create and register multiple clients


client1 := NewTestClient("https://example1.com/events", "chat-1",
"https://webhook.com", mockDB)
client2 := NewTestClient("https://example2.com/events", "chat-2",
"https://webhook.com", mockDB)
client3 := NewTestClient("https://example3.com/events", "chat-3",
"https://webhook.com", mockDB)

client1.Start()
client2.Start()
client3.Start()

// Allow time for registration


time.Sleep(50 * time.Millisecond)

// Verify all clients are registered


ClientRegistry.mutex.RLock()
assert.Equal(t, 3, len(ClientRegistry.clients), "Expected 3 registered
clients")
ClientRegistry.mutex.RUnlock()

// Stop one client


success := ClientRegistry.Unregister(client2.URL, client2.ChatID)
assert.True(t, success, "Expected successful unregistration")

// Verify client count


ClientRegistry.mutex.RLock()
assert.Equal(t, 2, len(ClientRegistry.clients), "Expected 2 registered
clients after unregistering one")
ClientRegistry.mutex.RUnlock()
// Stop remaining clients
client1.Stop()
client3.Stop()

// Allow time for unregistration


time.Sleep(50 * time.Millisecond)

// Verify all clients are unregistered


ClientRegistry.mutex.RLock()
assert.Equal(t, 0, len(ClientRegistry.clients), "Expected 0 registered
clients after stopping all")
ClientRegistry.mutex.RUnlock()
}

func TestClient_StoreEvent(t *testing.T) {


tests := []struct {
name string
eventData map[string]string
dbError error
}{
{
name: "Valid JSON Data",
eventData: map[string]string{
"id": "1",
"event": "update",
"data": "{\"key\":\"value\"}",
},
dbError: nil,
},
{
name: "Invalid JSON Data",
eventData: map[string]string{
"id": "2",
"event": "update",
"data": "{\"key\":\"value\"",
},
dbError: nil,
},
{
name: "Plain Text Data",
eventData: map[string]string{
"id": "3",
"event": "message",
"data": "This is a plain text message",
},
dbError: nil,
},
{
name: "Empty Data",
eventData: map[string]string{
"id": "4",
"event": "ping",
"data": "",
},
dbError: nil,
},
{
name: "Database Error",
eventData: map[string]string{
"id": "5",
"event": "update",
"data": "{\"key\":\"value\"}",
},
dbError: fmt.Errorf("database error"),
},
}

for _, tt := range tests {


t.Run(tt.name, func(t *testing.T) {
mockDB := new(MockDatabase)

// Set up mock expectations


if tt.dbError == nil {
mockID := uuid.New()
mockDB.On("CreateSSEMessageLog", mock.Anything, "chat-123",
"https://example.com/events", "https://webhook.com").Return(mockID, nil)
} else {
mockDB.On("CreateSSEMessageLog", mock.Anything, "chat-123",
"https://example.com/events", "https://webhook.com").Return(nil, tt.dbError)
}

client := NewTestClient("https://example.com/events", "chat-123",


"https://webhook.com", mockDB)

// Call storeEvent
err := client.storeEvent(tt.eventData)

// Check result
if tt.dbError == nil {
assert.NoError(t, err)
} else {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.dbError.Error())
}

// Verify expectations
mockDB.AssertExpectations(t)
})
}
}

You might also like