package proxy
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Race Condition Tests for TrafficLogger ---
func TestTrafficLogger_ConcurrentLogAndQuery(t *testing.T) {
logger := NewTrafficLogger(100)
var wg sync.WaitGroup
// 10 goroutines writing different log types concurrently
for w := 0; w < 10; w++ {
wg.Add(1)
go func(writerID int) {
defer wg.Done()
for i := 0; i < 50; i++ {
logger.LogHTTP(HTTPLogEntry{
ID: fmt.Sprintf("http-%d-%d", writerID, i),
Timestamp: time.Now(),
Method: "GET",
URL: fmt.Sprintf("/api/test/%d", i),
StatusCode: 200,
})
logger.LogError(FrontendError{
ID: fmt.Sprintf("err-%d-%d", writerID, i),
Timestamp: time.Now(),
Message: fmt.Sprintf("error from writer %d", writerID),
})
logger.LogCustom(CustomLog{
ID: fmt.Sprintf("custom-%d-%d", writerID, i),
Timestamp: time.Now(),
Level: "info",
Message: "test",
})
}
}(w)
}
// 5 concurrent readers
for r := 0; r < 5; r++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 30; i++ {
_ = logger.Query(LogFilter{})
_ = logger.Query(LogFilter{Types: []LogEntryType{LogTypeHTTP}})
_ = logger.Query(LogFilter{ErrorsOnly: true})
_ = logger.Stats()
}
}()
}
wg.Wait()
stats := logger.Stats()
assert.Greater(t, stats.TotalEntries, int64(0))
assert.LessOrEqual(t, stats.AvailableEntries, int64(100))
}
func TestTrafficLogger_ConcurrentLogAndClear(t *testing.T) {
logger := NewTrafficLogger(50)
var wg sync.WaitGroup
// Writers
for w := 0; w < 5; w++ {
wg.Add(1)
go func(writerID int) {
defer wg.Done()
for i := 0; i < 100; i++ {
logger.LogHTTP(HTTPLogEntry{
ID: fmt.Sprintf("log-%d-%d", writerID, i),
Timestamp: time.Now(),
Method: "POST",
URL: "/api/data",
})
}
}(w)
}
// Concurrent clears
for c := 0; c < 3; c++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
logger.Clear()
time.Sleep(time.Millisecond)
}
}()
}
wg.Wait()
// Should not panic or deadlock
_ = logger.Stats()
_ = logger.Query(LogFilter{})
}
func TestTrafficLogger_ConcurrentAllLogTypes(t *testing.T) {
logger := NewTrafficLogger(200)
var wg sync.WaitGroup
now := time.Now()
// Each goroutine logs a different type
logFuncs := []func(){
func() {
logger.LogHTTP(HTTPLogEntry{Timestamp: now, Method: "GET", URL: "/test"})
},
func() {
logger.LogError(FrontendError{Timestamp: now, Message: "test error"})
},
func() {
logger.LogPerformance(PerformanceMetric{Timestamp: now})
},
func() {
logger.LogCustom(CustomLog{Timestamp: now, Level: "info", Message: "test"})
},
func() {
logger.LogScreenshot(Screenshot{Timestamp: now, Name: "test"})
},
func() {
logger.LogExecution(ExecutionResult{Timestamp: now, Code: "alert(1)"})
},
func() {
logger.LogResponse(ExecutionResponse{Timestamp: now, Success: true})
},
func() {
logger.LogInteraction(InteractionEvent{Timestamp: now, EventType: "click"})
},
func() {
logger.LogMutation(MutationEvent{Timestamp: now, MutationType: "added"})
},
func() {
logger.LogPanelMessage(PanelMessage{Timestamp: now, Message: "hello"})
},
}
for _, fn := range logFuncs {
for i := 0; i < 5; i++ {
wg.Add(1)
logFn := fn
go func() {
defer wg.Done()
for j := 0; j < 20; j++ {
logFn()
}
}()
}
}
wg.Wait()
stats := logger.Stats()
assert.Greater(t, stats.TotalEntries, int64(0))
}
// --- Shutdown-Under-Load Tests for ProxyManager ---
func TestProxyManager_ShutdownUnderLoad(t *testing.T) {
pm := NewProxyManager()
ctx := context.Background()
// Create several proxies
proxyIDs := make([]string, 5)
for i := 0; i < 5; i++ {
id := fmt.Sprintf("load-%d", i)
proxyIDs[i] = id
config := ProxyConfig{
ID: id,
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, err := pm.Create(ctx, config)
require.NoError(t, err)
}
require.Equal(t, int64(5), pm.ActiveCount())
// Start concurrent operations on the proxies
var wg sync.WaitGroup
// Goroutines continuously listing and getting proxies
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 50; j++ {
_ = pm.List()
_, _ = pm.Get(proxyIDs[id%len(proxyIDs)])
}
}(i)
}
// Shutdown while operations are in-flight
shutdownErr := make(chan error, 1)
go func() {
time.Sleep(5 * time.Millisecond) // Let the reads start
shutdownErr <- pm.Shutdown(ctx)
}()
wg.Wait()
err := <-shutdownErr
assert.NoError(t, err)
assert.Equal(t, int64(0), pm.ActiveCount())
}
func TestProxyManager_ShutdownWithTimeout(t *testing.T) {
pm := NewProxyManager()
ctx := context.Background()
// Create proxies
for i := 0; i < 3; i++ {
config := ProxyConfig{
ID: fmt.Sprintf("timeout-%d", i),
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, err := pm.Create(ctx, config)
require.NoError(t, err)
}
// Shutdown with a very short timeout
shortCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel()
_ = pm.Shutdown(shortCtx)
// Even if some proxies time out, the manager should be in shutdown state
assert.True(t, pm.shuttingDown.Load())
}
func TestProxyManager_ConcurrentCreateDuringShutdown(t *testing.T) {
pm := NewProxyManager()
ctx := context.Background()
// Create initial proxies
for i := 0; i < 3; i++ {
config := ProxyConfig{
ID: fmt.Sprintf("init-%d", i),
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, err := pm.Create(ctx, config)
require.NoError(t, err)
}
var wg sync.WaitGroup
createErrors := make([]error, 10)
// Start shutdown
wg.Add(1)
go func() {
defer wg.Done()
_ = pm.Shutdown(ctx)
}()
// Try to create proxies concurrently with shutdown
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
config := ProxyConfig{
ID: fmt.Sprintf("during-shutdown-%d", id),
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, err := pm.Create(ctx, config)
createErrors[id] = err
}(i)
}
wg.Wait()
// At least some creates should have been rejected
rejectedCount := 0
for _, err := range createErrors {
if err != nil {
rejectedCount++
}
}
// After shutdown completes, all creates should be rejected
config := ProxyConfig{
ID: "post-shutdown",
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, err := pm.Create(ctx, config)
assert.Error(t, err, "create after shutdown should fail")
}
func TestProxyManager_StopAllUnderLoad(t *testing.T) {
pm := NewProxyManager()
ctx := context.Background()
// Create proxies
for i := 0; i < 5; i++ {
config := ProxyConfig{
ID: fmt.Sprintf("stopall-%d", i),
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, err := pm.Create(ctx, config)
require.NoError(t, err)
}
var wg sync.WaitGroup
// Concurrent list/get operations
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 30; j++ {
_ = pm.List()
_ = pm.ActiveCount()
_ = pm.TotalStarted()
}
}()
}
// StopAll while reads are happening
var stoppedIDs []string
var stopErr error
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(2 * time.Millisecond)
stoppedIDs, stopErr = pm.StopAll(ctx)
}()
wg.Wait()
assert.NoError(t, stopErr)
assert.Equal(t, 5, len(stoppedIDs))
// Unlike Shutdown, StopAll should allow new proxies
config := ProxyConfig{
ID: "after-stopall",
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
proxy, err := pm.Create(ctx, config)
assert.NoError(t, err)
if proxy != nil {
pm.Stop(ctx, "after-stopall")
}
}
func TestProxyManager_ConcurrentStopSameProxy(t *testing.T) {
pm := NewProxyManager()
ctx := context.Background()
config := ProxyConfig{
ID: "double-stop",
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, err := pm.Create(ctx, config)
require.NoError(t, err)
var wg sync.WaitGroup
errors := make([]error, 5)
// 5 goroutines all try to stop the same proxy
for i := 0; i < 5; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
errors[idx] = pm.Stop(ctx, "double-stop")
}(i)
}
wg.Wait()
// Exactly one should succeed, others should get ErrProxyNotFound
successCount := 0
for _, err := range errors {
if err == nil {
successCount++
}
}
// At least one succeeded (might be more if the race resolves before
// the delete, but the proxy should still be stopped cleanly)
assert.GreaterOrEqual(t, successCount, 1)
assert.Equal(t, int64(0), pm.ActiveCount())
}
// --- Race Condition Tests for ProxyManager Registry ---
func TestProxyManager_ConcurrentCreateAndList(t *testing.T) {
pm := NewProxyManager()
ctx := context.Background()
var wg sync.WaitGroup
// Create proxies concurrently
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
config := ProxyConfig{
ID: fmt.Sprintf("race-create-%d", id),
TargetURL: "http://localhost:9999",
ListenPort: 0,
MaxLogSize: 100,
}
_, _ = pm.Create(ctx, config)
}(i)
}
// List concurrently
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 20; j++ {
_ = pm.List()
_ = pm.ActiveCount()
}
}()
}
wg.Wait()
// Cleanup
for _, p := range pm.List() {
pm.Stop(ctx, p.ID)
}
}