package proxy import ( "context" "errors" "fmt" "io" "os" "strings" "sync" "testing" "time" "UAmask/internal/model" "github.com/sirupsen/logrus" ) func TestMain(main *testing.M) { logrus.SetOutput(io.Discard) os.Exit(main.Run()) } type acceptResult struct { session *model.AcceptedSession err error } type ingressStub struct { results chan acceptResult delivered chan *model.AcceptedSession closed chan struct{} once sync.Once } func newIngressStub() *ingressStub { return &ingressStub{ results: make(chan acceptResult, 4), delivered: make(chan *model.AcceptedSession, 4), closed: make(chan struct{}), } } func (stub *ingressStub) Accept() (*model.AcceptedSession, error) { select { case result := <-stub.results: select { case stub.delivered <- result.session: default: } return result.session, result.err case <-stub.closed: return nil, context.Canceled } } type contextBlockingManager struct { mu sync.Mutex handled []*model.AcceptedSession started chan *model.AcceptedSession } type orderedContextManager struct { ingress *ingressStub started chan struct{} observed chan bool } func (manager *orderedContextManager) Handle(accepted *model.AcceptedSession) { manager.HandleContext(context.Background(), accepted) } func (manager *orderedContextManager) HandleContext(ctx context.Context, _ *model.AcceptedSession) { close(manager.started) <-ctx.Done() select { case <-manager.ingress.closed: manager.observed <- true default: manager.observed <- false } } func (manager *contextBlockingManager) Handle(accepted *model.AcceptedSession) { manager.HandleContext(context.Background(), accepted) } func (manager *contextBlockingManager) HandleContext(ctx context.Context, accepted *model.AcceptedSession) { manager.mu.Lock() manager.handled = append(manager.handled, accepted) manager.mu.Unlock() manager.started <- accepted <-ctx.Done() } func (manager *contextBlockingManager) Handled() []*model.AcceptedSession { manager.mu.Lock() defer manager.mu.Unlock() return append([]*model.AcceptedSession(nil), manager.handled...) } func (stub *ingressStub) Close() error { stub.once.Do(func() { close(stub.closed) }) return nil } type blockingManager struct { started chan *model.AcceptedSession release chan struct{} } func (manager *blockingManager) Handle(accepted *model.AcceptedSession) { manager.started <- accepted <-manager.release } func TestServerRunContextWaitsForAcceptedSessions(t *testing.T) { for _, poolSize := range []int{0, 2} { poolSize := poolSize t.Run(fmt.Sprintf("LIFE-SHUTDOWN-201/pool-%d", poolSize), func(t *testing.T) { ingress := newIngressStub() manager := &blockingManager{ started: make(chan *model.AcceptedSession, 1), release: make(chan struct{}), } server := NewServer(poolSize, ingress, manager) ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- server.RunContext(ctx) }() accepted := &model.AcceptedSession{Target: model.Target{Address: "203.0.113.10:443"}} ingress.results <- acceptResult{session: accepted} if got := waitValue(t, manager.started); got != accepted { t.Fatalf("manager received %p, want %p", got, accepted) } cancel() waitClosed(t, ingress.closed) select { case err := <-done: t.Fatalf("server returned before accepted session completed: %v", err) default: } close(manager.release) if err := waitValue(t, done); err != nil { t.Fatalf("RunContext returned error on cancellation: %v", err) } }) } } func TestServerRunContextReturnsFatalAcceptError(t *testing.T) { ingress := newIngressStub() ingress.results <- acceptResult{err: errors.New("listener failed")} server := NewServer(0, ingress, &blockingManager{ started: make(chan *model.AcceptedSession, 1), release: make(chan struct{}), }) err := server.RunContext(context.Background()) if err == nil || !strings.Contains(err.Error(), "listener failed") { t.Fatalf("LIFE-ERROR-201 RunContext error = %v, want listener failure", err) } waitClosed(t, ingress.closed) } func TestServerWorkerPoolSkipsQueuedSessionsAfterCancellation(t *testing.T) { ingress := newIngressStub() manager := &contextBlockingManager{started: make(chan *model.AcceptedSession, 2)} server := NewServer(1, ingress, manager) ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- server.RunContext(ctx) }() first := &model.AcceptedSession{Target: model.Target{Address: "203.0.113.10:80"}} second := &model.AcceptedSession{Target: model.Target{Address: "203.0.113.11:80"}} ingress.results <- acceptResult{session: first} if got := waitValue(t, manager.started); got != first { t.Fatalf("LIFE-POOL-202 first handled session = %p, want %p", got, first) } if got := waitValue(t, ingress.delivered); got != first { t.Fatalf("LIFE-POOL-202 first delivered session = %p, want %p", got, first) } ingress.results <- acceptResult{session: second} if got := waitValue(t, ingress.delivered); got != second { t.Fatalf("LIFE-POOL-202 delivered session = %p, want %p", got, second) } cancel() if err := waitValue(t, done); err != nil { t.Fatalf("LIFE-POOL-202 RunContext: %v", err) } handled := manager.Handled() if len(handled) != 1 || handled[0] != first { t.Fatalf("LIFE-POOL-202 handled sessions = %v, want only first", handled) } } func TestServerFatalAcceptErrorCancelsActiveAndQueuedSessions(t *testing.T) { for _, poolSize := range []int{0, 1} { poolSize := poolSize t.Run(fmt.Sprintf("LIFE-ERROR-203/pool-%d", poolSize), func(t *testing.T) { ingress := newIngressStub() manager := &contextBlockingManager{started: make(chan *model.AcceptedSession, 2)} server := NewServer(poolSize, ingress, manager) done := make(chan error, 1) go func() { done <- server.RunContext(context.Background()) }() first := &model.AcceptedSession{Target: model.Target{Address: "203.0.113.20:80"}} ingress.results <- acceptResult{session: first} if got := waitValue(t, manager.started); got != first { t.Fatalf("first handled session = %p, want %p", got, first) } if got := waitValue(t, ingress.delivered); got != first { t.Fatalf("first delivered session = %p, want %p", got, first) } var second *model.AcceptedSession if poolSize > 0 { second = &model.AcceptedSession{Target: model.Target{Address: "203.0.113.21:80"}} ingress.results <- acceptResult{session: second} if got := waitValue(t, ingress.delivered); got != second { t.Fatalf("queued delivered session = %p, want %p", got, second) } } ingress.results <- acceptResult{err: errors.New("listener failed")} err := waitValue(t, done) if err == nil || !strings.Contains(err.Error(), "listener failed") { t.Fatalf("RunContext error = %v, want listener failure", err) } handled := manager.Handled() if len(handled) != 1 || handled[0] != first { t.Fatalf("handled sessions = %v, want only first; queued=%p", handled, second) } }) } } func TestServerClosesIngressBeforeCancellingHandlers(t *testing.T) { ingress := newIngressStub() manager := &orderedContextManager{ ingress: ingress, started: make(chan struct{}), observed: make(chan bool, 1), } server := NewServer(0, ingress, manager) ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- server.RunContext(ctx) }() ingress.results <- acceptResult{session: &model.AcceptedSession{Target: model.Target{Address: "203.0.113.30:80"}}} waitClosed(t, manager.started) cancel() if observed := waitValue(t, manager.observed); !observed { t.Fatal("LIFE-ORDER-204 handler observed cancellation before ingress was closed") } if err := waitValue(t, done); err != nil { t.Fatalf("LIFE-ORDER-204 RunContext: %v", err) } } func waitClosed(t *testing.T, channel <-chan struct{}) { t.Helper() select { case <-channel: case <-time.After(time.Second): t.Fatal("timed out waiting for channel to close") } } func waitValue[T any](t *testing.T, channel <-chan T) T { t.Helper() select { case value := <-channel: return value case <-time.After(time.Second): t.Fatal("timed out waiting for channel value") var zero T return zero } }