From e63c93ec417863dc8dabd2b9c5862914ebe89fd1 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Fri, 19 Jun 2026 15:09:49 +0300 Subject: [PATCH] =?UTF-8?q?test:=20=D0=BF=D0=BE=D0=BA=D1=80=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D0=B5=20multi.Provider=20=D0=B8=20shape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit multi: round-robin биекция, маршрутизация Handle/Reset, max backoff, panic. shape: passthrough при interval=0, форвардинг WriteTo, обёртка listener. --- internal/provider/multi/multi_test.go | 124 ++++++++++++++++++++++++++ internal/wire/shape/shape_test.go | 97 ++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 internal/provider/multi/multi_test.go create mode 100644 internal/wire/shape/shape_test.go diff --git a/internal/provider/multi/multi_test.go b/internal/provider/multi/multi_test.go new file mode 100644 index 0000000..5ada231 --- /dev/null +++ b/internal/provider/multi/multi_test.go @@ -0,0 +1,124 @@ +package multi + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/samosvalishe/free-turn-proxy/internal/provider" +) + +// fakeProvider записывает streamID, с которыми его звали, и кодирует свой индекс +// в выданных Credentials - чтобы проверить и маршрут, и значение. +type fakeProvider struct { + idx int + backoff int64 + gotCreds []int + gotHandle []int + gotReset []int + authResult bool +} + +func (f *fakeProvider) GetCredentials(_ context.Context, streamID int) (provider.Credentials, error) { + f.gotCreds = append(f.gotCreds, streamID) + return provider.Credentials{User: fmt.Sprintf("p%d-s%d", f.idx, streamID)}, nil +} +func (f *fakeProvider) IsAuthError(error) bool { return f.authResult } +func (f *fakeProvider) HandleAuthError(s int) bool { f.gotHandle = append(f.gotHandle, s); return true } +func (f *fakeProvider) ResetErrors(s int) { f.gotReset = append(f.gotReset, s) } +func (f *fakeProvider) BackoffUntilUnix() int64 { return f.backoff } +func (f *fakeProvider) Name() string { return fmt.Sprintf("fake%d", f.idx) } + +func newFakes(n int) ([]provider.Provider, []*fakeProvider) { + ps := make([]provider.Provider, n) + fs := make([]*fakeProvider, n) + for i := range ps { + f := &fakeProvider{idx: i} + ps[i], fs[i] = f, f + } + return ps, fs +} + +func TestNewPanicsOnEmpty(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("expected panic on empty providers") + } + }() + New(nil) +} + +func TestGetCredentialsRoundRobin(t *testing.T) { + ps, fs := newFakes(3) + m := New(ps) + + // 3 провайдера x 10 стримов = 30 глобальных streamID. + for sid := 1; sid <= 30; sid++ { + creds, err := m.GetCredentials(context.Background(), sid) + if err != nil { + t.Fatalf("streamID %d: %v", sid, err) + } + wantIdx := (sid - 1) % 3 + wantInner := ((sid - 1) / 3) + 1 + want := fmt.Sprintf("p%d-s%d", wantIdx, wantInner) + if creds.User != want { + t.Fatalf("streamID %d: got %q, want %q", sid, creds.User, want) + } + } + + // Каждый провайдер получил ровно innerID 1..10. + for i, f := range fs { + if len(f.gotCreds) != 10 { + t.Fatalf("provider %d got %d calls, want 10", i, len(f.gotCreds)) + } + for j, inner := range f.gotCreds { + if inner != j+1 { + t.Fatalf("provider %d call %d: innerID %d, want %d", i, j, inner, j+1) + } + } + } +} + +func TestHandleAndResetRouting(t *testing.T) { + ps, fs := newFakes(2) + m := New(ps) + + // streamID 5: idx=(5-1)%2=0, inner=(5-1)/2+1=3. + m.HandleAuthError(5) + m.ResetErrors(5) + if len(fs[0].gotHandle) != 1 || fs[0].gotHandle[0] != 3 { + t.Fatalf("HandleAuthError routed wrong: %v", fs[0].gotHandle) + } + if len(fs[0].gotReset) != 1 || fs[0].gotReset[0] != 3 { + t.Fatalf("ResetErrors routed wrong: %v", fs[0].gotReset) + } + if len(fs[1].gotHandle) != 0 { + t.Fatalf("provider 1 should not be touched: %v", fs[1].gotHandle) + } +} + +func TestBackoffUntilUnixMax(t *testing.T) { + ps, fs := newFakes(3) + fs[0].backoff = 100 + fs[1].backoff = 500 + fs[2].backoff = 300 + if got := New(ps).BackoffUntilUnix(); got != 500 { + t.Fatalf("BackoffUntilUnix = %d, want 500 (max)", got) + } +} + +func TestIsAuthErrorDelegatesToFirst(t *testing.T) { + ps, fs := newFakes(2) + fs[0].authResult = true + if !New(ps).IsAuthError(errors.New("x")) { + t.Fatal("IsAuthError should delegate to providers[0]") + } +} + +func TestName(t *testing.T) { + ps, _ := newFakes(4) + if got := New(ps).Name(); got != "multi(4)" { + t.Fatalf("Name = %q, want multi(4)", got) + } +} diff --git a/internal/wire/shape/shape_test.go b/internal/wire/shape/shape_test.go new file mode 100644 index 0000000..a536f85 --- /dev/null +++ b/internal/wire/shape/shape_test.go @@ -0,0 +1,97 @@ +package shape + +import ( + "net" + "testing" + "time" + + dtlsnet "github.com/pion/dtls/v3/pkg/net" +) + +type fakeAddr struct{} + +func (fakeAddr) Network() string { return "udp" } +func (fakeAddr) String() string { return "fake" } + +type fakePacketConn struct { + writes [][]byte +} + +func (*fakePacketConn) ReadFrom([]byte) (int, net.Addr, error) { return 0, fakeAddr{}, nil } +func (f *fakePacketConn) WriteTo(p []byte, _ net.Addr) (int, error) { + b := make([]byte, len(p)) + copy(b, p) + f.writes = append(f.writes, b) + return len(p), nil +} +func (*fakePacketConn) Close() error { return nil } +func (*fakePacketConn) LocalAddr() net.Addr { return fakeAddr{} } +func (*fakePacketConn) SetDeadline(time.Time) error { return nil } +func (*fakePacketConn) SetReadDeadline(time.Time) error { return nil } +func (*fakePacketConn) SetWriteDeadline(time.Time) error { return nil } + +func TestWrapPacketConnPassthrough(t *testing.T) { + f := &fakePacketConn{} + if got := WrapPacketConn(f, 0); got != f { + t.Fatalf("interval=0 must return original conn, got %T", got) + } +} + +func TestWrapPacketConnForwards(t *testing.T) { + f := &fakePacketConn{} + wrapped := WrapPacketConn(f, time.Millisecond) + if _, ok := wrapped.(*ShapedPacketConn); !ok { + t.Fatalf("interval>0 must return *ShapedPacketConn, got %T", wrapped) + } + n, err := wrapped.WriteTo([]byte("hello"), fakeAddr{}) + if err != nil || n != 5 { + t.Fatalf("WriteTo = (%d, %v)", n, err) + } + if len(f.writes) != 1 || string(f.writes[0]) != "hello" { + t.Fatalf("inner writes = %v", f.writes) + } +} + +func TestShaperWaitNoIntervalReturns(t *testing.T) { + done := make(chan struct{}) + go func() { New(0).Wait(); close(done) }() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("Wait blocked with interval=0") + } +} + +type fakeListener struct { + inner net.PacketConn + addr net.Addr +} + +func (l *fakeListener) Accept() (net.PacketConn, net.Addr, error) { return l.inner, l.addr, nil } +func (*fakeListener) Close() error { return nil } +func (l *fakeListener) Addr() net.Addr { return l.addr } + +func TestWrapPacketListenerPassthrough(t *testing.T) { + var l dtlsnet.PacketListener = &fakeListener{addr: fakeAddr{}} + if got := WrapPacketListener(l, 0); got != l { + t.Fatalf("interval=0 must return original listener") + } +} + +func TestWrapPacketListenerWrapsAccepted(t *testing.T) { + f := &fakePacketConn{} + l := WrapPacketListener(&fakeListener{inner: f, addr: fakeAddr{}}, time.Millisecond) + pc, _, err := l.Accept() + if err != nil { + t.Fatal(err) + } + if _, ok := pc.(*ShapedPacketConn); !ok { + t.Fatalf("accepted conn must be *ShapedPacketConn, got %T", pc) + } + if _, err := pc.WriteTo([]byte("x"), fakeAddr{}); err != nil { + t.Fatal(err) + } + if len(f.writes) != 1 { + t.Fatalf("inner writes = %d, want 1", len(f.writes)) + } +}