diff options
Diffstat (limited to 'hotline/server_test.go')
| -rw-r--r-- | hotline/server_test.go | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/hotline/server_test.go b/hotline/server_test.go index d0e2372..574aae7 100644 --- a/hotline/server_test.go +++ b/hotline/server_test.go @@ -3,12 +3,15 @@ package hotline import ( "bytes" "context" + "encoding/binary" "fmt" "github.com/stretchr/testify/assert" "io" "log/slog" "os" + "strings" "testing" + "time" ) type mockReadWriter struct { @@ -181,3 +184,583 @@ func TestServer_handleFileTransfer(t *testing.T) { }) } } + +func TestParseTrackerPassword(t *testing.T) { + tests := []struct { + name string + trackerAddr string + wantPassword string + }{ + { + name: "tracker address with password", + trackerAddr: "tracker.example.com:5500:mypassword", + wantPassword: "mypassword", + }, + { + name: "tracker address without password", + trackerAddr: "tracker.example.com:5500", + wantPassword: "", + }, + { + name: "tracker address with empty password", + trackerAddr: "tracker.example.com:5500:", + wantPassword: "", + }, + { + name: "tracker address with password containing special characters", + trackerAddr: "tracker.example.com:5500:pass@word#123", + wantPassword: "pass@word#123", + }, + { + name: "tracker address with password containing colons", + trackerAddr: "tracker.example.com:5500:pass:word:123", + wantPassword: "pass:word:123", + }, + { + name: "IPv4 address with password", + trackerAddr: "192.168.1.100:5500:secret", + wantPassword: "secret", + }, + { + name: "IPv4 address without password", + trackerAddr: "192.168.1.100:5500", + wantPassword: "", + }, + { + name: "malformed address - no port", + trackerAddr: "tracker.example.com", + wantPassword: "", + }, + { + name: "malformed address - empty string", + trackerAddr: "", + wantPassword: "", + }, + { + name: "malformed address - only colons", + trackerAddr: ":::", + wantPassword: ":", + }, + { + name: "IPv6 address handling (edge case - not properly supported)", + trackerAddr: "[::1]:5500:password", + wantPassword: "1]:5500:password", // IPv6 addresses aren't properly handled by simple colon splitting + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := parseTrackerPassword(tt.trackerAddr) + assert.Equal(t, tt.wantPassword, got) + }) + } +} + +// MockTrackerRegistrar is a mock implementation of TrackerRegistrar for testing +type MockTrackerRegistrar struct { + RegisterCalls []RegisterCall + RegisterFunc func(tracker string, registration *TrackerRegistration) error +} + +type RegisterCall struct { + Tracker string + Registration *TrackerRegistration +} + +func (m *MockTrackerRegistrar) Register(tracker string, registration *TrackerRegistration) error { + // Record the call + m.RegisterCalls = append(m.RegisterCalls, RegisterCall{ + Tracker: tracker, + Registration: registration, + }) + + // Use custom function if provided, otherwise return nil (success) + if m.RegisterFunc != nil { + return m.RegisterFunc(tracker, registration) + } + return nil +} + +func (m *MockTrackerRegistrar) Reset() { + m.RegisterCalls = nil + m.RegisterFunc = nil +} + +func TestServer_registerWithTrackers(t *testing.T) { + tests := []struct { + name string + config Config + wantImmediateRegistration bool + wantTrackerCalls []string + mockRegisterFunc func(tracker string, registration *TrackerRegistration) error + expectError bool + }{ + { + name: "disabled tracker registration", + config: Config{ + EnableTrackerRegistration: false, + Trackers: []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"}, + }, + wantImmediateRegistration: false, + wantTrackerCalls: []string{}, + }, + { + name: "enabled tracker registration with multiple trackers", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"}, + Name: "Test Server", + Description: "Test Description", + }, + wantImmediateRegistration: true, + wantTrackerCalls: []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"}, + }, + { + name: "enabled tracker registration with empty tracker list", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{}, + Name: "Test Server", + Description: "Test Description", + }, + wantImmediateRegistration: true, + wantTrackerCalls: []string{}, + }, + { + name: "tracker registration with network errors", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{"tracker1.example.com:5500"}, + Name: "Test Server", + Description: "Test Description", + }, + wantImmediateRegistration: true, + wantTrackerCalls: []string{"tracker1.example.com:5500"}, + mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error { + return assert.AnError // Simulate network error + }, + expectError: false, // Errors are logged but don't stop the function + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock registrar + mockRegistrar := &MockTrackerRegistrar{ + RegisterFunc: tt.mockRegisterFunc, + } + + // Create server with mock registrar + server, err := NewServer( + WithConfig(tt.config), + WithLogger(NewTestLogger()), + WithTrackerRegistrar(mockRegistrar), + ) + assert.NoError(t, err) + + // Create a context that we can cancel + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Start the registerWithTrackers function in a goroutine + done := make(chan struct{}) + go func() { + defer close(done) + server.registerWithTrackers(ctx) + }() + + // Give it a moment to do the immediate registration + time.Sleep(100 * time.Millisecond) + + // Cancel the context to stop the goroutine + cancel() + + // Wait for the goroutine to finish (should be quick after cancellation) + select { + case <-done: + // Success + case <-time.After(1 * time.Second): + t.Fatal("registerWithTrackers did not exit after context cancellation") + } + + // Verify the calls made to the mock registrar + assert.Len(t, mockRegistrar.RegisterCalls, len(tt.wantTrackerCalls)) + + for i, expectedTracker := range tt.wantTrackerCalls { + if i < len(mockRegistrar.RegisterCalls) { + call := mockRegistrar.RegisterCalls[i] + assert.Equal(t, expectedTracker, call.Tracker) + assert.Equal(t, tt.config.Name, call.Registration.Name) + assert.Equal(t, tt.config.Description, call.Registration.Description) + assert.Equal(t, parseTrackerPassword(expectedTracker), call.Registration.Password) + } + } + }) + } +} + +func TestServer_registerWithTrackers_ContextCancellation(t *testing.T) { + tests := []struct { + name string + cancelAfter time.Duration + expectedCalls int // Number of expected registration calls before cancellation + trackerCount int + }{ + { + name: "immediate cancellation", + cancelAfter: 10 * time.Millisecond, + expectedCalls: 2, // Should complete immediate registration + trackerCount: 2, + }, + { + name: "cancellation after first ticker", + cancelAfter: 100 * time.Millisecond, + expectedCalls: 2, // Should only do immediate registration within 100ms + trackerCount: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRegistrar := &MockTrackerRegistrar{} + config := Config{ + EnableTrackerRegistration: true, + Trackers: make([]string, tt.trackerCount), + Name: "Test Server", + Description: "Test Description", + } + + // Fill trackers array + for i := 0; i < tt.trackerCount; i++ { + config.Trackers[i] = fmt.Sprintf("tracker%d.example.com:5500", i+1) + } + + server, err := NewServer( + WithConfig(config), + WithLogger(NewTestLogger()), + WithTrackerRegistrar(mockRegistrar), + ) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := make(chan struct{}) + go func() { + defer close(done) + server.registerWithTrackers(ctx) + }() + + // Wait for the specified time then cancel + time.Sleep(tt.cancelAfter) + cancel() + + // Wait for graceful shutdown + select { + case <-done: + // Success + case <-time.After(1 * time.Second): + t.Fatal("registerWithTrackers did not exit after context cancellation") + } + + // Verify that the function respects context cancellation + assert.Equal(t, tt.expectedCalls, len(mockRegistrar.RegisterCalls)) + }) + } +} + +func TestServer_registerWithTrackers_PeriodicRegistration(t *testing.T) { + t.Skip("Skipping timing-sensitive test - would take 5+ minutes to run reliably") + + // This test would verify that periodic re-registration happens every trackerUpdateFrequency seconds + // but it's impractical to run in normal test suites due to the 300-second interval + + mockRegistrar := &MockTrackerRegistrar{} + config := Config{ + EnableTrackerRegistration: true, + Trackers: []string{"tracker1.example.com:5500"}, + Name: "Test Server", + Description: "Test Description", + } + + server, err := NewServer( + WithConfig(config), + WithLogger(NewTestLogger()), + WithTrackerRegistrar(mockRegistrar), + ) + assert.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + done := make(chan struct{}) + go func() { + defer close(done) + server.registerWithTrackers(ctx) + }() + + // Wait for timeout or completion + <-ctx.Done() + + // Should have done immediate registration only (1 call) in 10 seconds + // since trackerUpdateFrequency is 300 seconds + assert.Equal(t, 1, len(mockRegistrar.RegisterCalls)) +} + +func TestServer_registerWithTrackers_ErrorHandling(t *testing.T) { + tests := []struct { + name string + trackers []string + mockRegisterFunc func(tracker string, registration *TrackerRegistration) error + expectPanic bool + }{ + { + name: "handles network errors gracefully", + trackers: []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"}, + mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error { + if tracker == "tracker1.example.com:5500" { + return fmt.Errorf("network error: connection refused") + } + return nil // Second tracker succeeds + }, + expectPanic: false, + }, + { + name: "handles all trackers failing", + trackers: []string{"tracker1.example.com:5500", "tracker2.example.com:5500"}, + mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error { + return fmt.Errorf("network error") + }, + expectPanic: false, + }, + { + name: "handles empty tracker addresses", + trackers: []string{"", "valid.tracker.com:5500", ""}, + mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error { + if tracker == "" { + return fmt.Errorf("invalid tracker address") + } + return nil + }, + expectPanic: false, + }, + { + name: "handles malformed tracker addresses", + trackers: []string{"invalid-address", "another:invalid", "valid.tracker.com:5500:password"}, + mockRegisterFunc: func(tracker string, registration *TrackerRegistration) error { + return nil // Accept all for this test + }, + expectPanic: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRegistrar := &MockTrackerRegistrar{ + RegisterFunc: tt.mockRegisterFunc, + } + + config := Config{ + EnableTrackerRegistration: true, + Trackers: tt.trackers, + Name: "Test Server", + Description: "Test Description", + } + + server, err := NewServer( + WithConfig(config), + WithLogger(NewTestLogger()), + WithTrackerRegistrar(mockRegistrar), + ) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if tt.expectPanic { + assert.Panics(t, func() { + server.registerWithTrackers(ctx) + }) + return + } + + done := make(chan struct{}) + go func() { + defer close(done) + server.registerWithTrackers(ctx) + }() + + // Give it time to process + time.Sleep(50 * time.Millisecond) + cancel() + + select { + case <-done: + // Success - function completed without panicking + case <-time.After(1 * time.Second): + t.Fatal("registerWithTrackers did not exit after context cancellation") + } + + // Verify all trackers were attempted + assert.Equal(t, len(tt.trackers), len(mockRegistrar.RegisterCalls)) + }) + } +} + +func TestServer_registerWithTrackers_EdgeCases(t *testing.T) { + tests := []struct { + name string + config Config + expectedCalls int + validateResult func(t *testing.T, calls []RegisterCall) + }{ + { + name: "server with zero port", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{"tracker.example.com:5500"}, + Name: "Test Server", + Description: "Test Description", + }, + expectedCalls: 1, + validateResult: func(t *testing.T, calls []RegisterCall) { + assert.Equal(t, uint16(0), binary.BigEndian.Uint16(calls[0].Registration.Port[:])) + }, + }, + { + name: "server with very long name and description", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{"tracker.example.com:5500"}, + Name: strings.Repeat("A", 255), // Max uint8 length + Description: strings.Repeat("B", 255), + }, + expectedCalls: 1, + validateResult: func(t *testing.T, calls []RegisterCall) { + assert.Equal(t, strings.Repeat("A", 255), calls[0].Registration.Name) + assert.Equal(t, strings.Repeat("B", 255), calls[0].Registration.Description) + }, + }, + { + name: "empty server name and description", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{"tracker.example.com:5500"}, + Name: "", + Description: "", + }, + expectedCalls: 1, + validateResult: func(t *testing.T, calls []RegisterCall) { + assert.Equal(t, "", calls[0].Registration.Name) + assert.Equal(t, "", calls[0].Registration.Description) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRegistrar := &MockTrackerRegistrar{} + + server, err := NewServer( + WithConfig(tt.config), + WithLogger(NewTestLogger()), + WithTrackerRegistrar(mockRegistrar), + ) + assert.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := make(chan struct{}) + go func() { + defer close(done) + server.registerWithTrackers(ctx) + }() + + time.Sleep(50 * time.Millisecond) + cancel() + + select { + case <-done: + // Success + case <-time.After(1 * time.Second): + t.Fatal("registerWithTrackers did not exit after context cancellation") + } + + assert.Equal(t, tt.expectedCalls, len(mockRegistrar.RegisterCalls)) + if tt.validateResult != nil { + tt.validateResult(t, mockRegistrar.RegisterCalls) + } + }) + } +} + +func TestServer_registerWithAllTrackers(t *testing.T) { + tests := []struct { + name string + config Config + expectRegistrationAttempt bool + expectedTrackerCalls []string + }{ + { + name: "disabled tracker registration", + config: Config{ + EnableTrackerRegistration: false, + Trackers: []string{"tracker1.example.com:5500"}, + }, + expectRegistrationAttempt: false, + expectedTrackerCalls: []string{}, + }, + { + name: "enabled tracker registration with multiple trackers", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"}, + Name: "Test Server", + Description: "Test Description", + }, + expectRegistrationAttempt: true, + expectedTrackerCalls: []string{"tracker1.example.com:5500", "tracker2.example.com:5500:password"}, + }, + { + name: "enabled tracker registration with empty tracker list", + config: Config{ + EnableTrackerRegistration: true, + Trackers: []string{}, + Name: "Test Server", + Description: "Test Description", + }, + expectRegistrationAttempt: true, + expectedTrackerCalls: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRegistrar := &MockTrackerRegistrar{} + + server, err := NewServer( + WithConfig(tt.config), + WithLogger(NewTestLogger()), + WithTrackerRegistrar(mockRegistrar), + ) + assert.NoError(t, err) + + // Call the extracted function directly + server.registerWithAllTrackers() + + // Verify the expected number of calls + assert.Equal(t, len(tt.expectedTrackerCalls), len(mockRegistrar.RegisterCalls)) + + // Verify each call + for i, expectedTracker := range tt.expectedTrackerCalls { + if i < len(mockRegistrar.RegisterCalls) { + call := mockRegistrar.RegisterCalls[i] + assert.Equal(t, expectedTracker, call.Tracker) + assert.Equal(t, tt.config.Name, call.Registration.Name) + assert.Equal(t, tt.config.Description, call.Registration.Description) + assert.Equal(t, parseTrackerPassword(expectedTracker), call.Registration.Password) + } + } + }) + } +} |