1package sessionwire
2
3import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "google.golang.org/protobuf/proto"
10
11 "github.com/bnema/vev/internal/protocol"
12 "github.com/bnema/vev/internal/protocol/wire"
13 "github.com/stretchr/testify/require"
14)
15
16// protoPipe is a scripted in-memory wire.Transport for preamble tests.
17type protoPipe struct {
18 mu sync.Mutex
19 in chan wire.Envelope
20 peer *protoPipe
21 closed bool
22}
23
24func newProtoPipePair() (*protoPipe, *protoPipe) {
25 a := &protoPipe{in: make(chan wire.Envelope, 16)}
26 b := &protoPipe{in: make(chan wire.Envelope, 16)}
27 a.peer, b.peer = b, a
28 return a, b
29}
30
31func (p *protoPipe) Send(envelope wire.Envelope) error {
32 p.mu.Lock()
33 peer, closed := p.peer, p.closed
34 p.mu.Unlock()
35 if closed {
36 return context.Canceled
37 }
38 select {
39 case peer.in <- envelope:
40 return nil
41 default:
42 return ErrUnsupportedSend
43 }
44}
45
46func (p *protoPipe) Recv() (wire.Envelope, error) {
47 envelope, ok := <-p.in
48 if !ok {
49 return wire.Envelope{}, context.DeadlineExceeded
50 }
51 return envelope, nil
52}
53
54func (p *protoPipe) Close() error {
55 p.mu.Lock()
56 defer p.mu.Unlock()
57 p.closed = true
58 return nil
59}
60
61// TestProtoPreambleHandshake proves the preamble exchange over raw
62// envelope transports: acceptance negotiates minima, mismatches refuse with
63// a typed response, and the deadline bounds a silent peer.
64func TestProtoPreambleHandshake(t *testing.T) {
65 t.Run("accept negotiates minima", func(t *testing.T) {
66 client, server := newProtoPipePair()
67 serverCeilings := protoCeilings{maxReceiveEnvelopeBytes: wire.AbsoluteEnvelopeLimit / 2, outputDataLimit: uint64(protocol.MaxOutputDataLen) / 2}
68 serverDone := make(chan protoCeilings, 1)
69 go func() {
70 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
71 defer cancel()
72 ceilings, err := runProtoServerPreamble(ctx, server, serverCeilings)
73 require.NoError(t, err)
74 serverDone <- ceilings
75 }()
76 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
77 defer cancel()
78 clientCeilings, err := runProtoClientPreamble(ctx, client, defaultProtoCeilings())
79 require.NoError(t, err)
80 require.Equal(t, serverCeilings, clientCeilings)
81 require.Equal(t, serverCeilings, <-serverDone)
82 })
83
84 t.Run("version mismatch refuses", func(t *testing.T) {
85 client, server := newProtoPipePair()
86 go func() {
87 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
88 defer cancel()
89 _, _ = runProtoServerPreamble(ctx, server, defaultProtoCeilings())
90 }()
91 bad := preambleRequestToWire(defaultProtoCeilings())
92 bad.Version = uint32(protocol.Version) + 1
93 raw, err := (proto.Marshal(bad))
94 require.NoError(t, err)
95 require.NoError(t, client.Send(wire.Envelope{Payload: raw}))
96 envelope, err := client.Recv()
97 require.NoError(t, err)
98 var response wire.PreambleResponse
99 require.NoError(t, (proto.UnmarshalOptions{DiscardUnknown: false}.Unmarshal(envelope.Payload, &response)))
100 require.False(t, response.GetAccepted())
101 require.Equal(t, uint32(3), response.GetRejection().GetCode())
102 })
103
104 t.Run("wrong magic refuses", func(t *testing.T) {
105 bad := preambleRequestToWire(defaultProtoCeilings())
106 bad.Magic = 0xDEADBEEF
107 _, err := checkPreambleRequest(bad)
108 require.ErrorIs(t, err, ErrPreambleRejected)
109 })
110
111 t.Run("wrong role refuses", func(t *testing.T) {
112 bad := preambleRequestToWire(defaultProtoCeilings())
113 bad.Role.Role = 2
114 _, err := checkPreambleRequest(bad)
115 require.ErrorIs(t, err, ErrPreambleRejected)
116
117 resp := preambleResponseToWire(true, defaultProtoCeilings(), 0)
118 resp.Role.Role = 1
119 _, err = checkPreambleResponse(resp)
120 require.ErrorIs(t, err, ErrPreambleRejected)
121 })
122
123 t.Run("oversized advertisement refuses", func(t *testing.T) {
124 bad := preambleRequestToWire(defaultProtoCeilings())
125 bad.MaxReceiveEnvelopeBytes = wire.AbsoluteEnvelopeLimit + 1
126 _, err := checkPreambleRequest(bad)
127 require.ErrorIs(t, err, ErrPreambleRejected)
128
129 bad = preambleRequestToWire(defaultProtoCeilings())
130 bad.OutputDataLimit = uint64(protocol.MaxOutputDataLen) + 1
131 _, err = checkPreambleRequest(bad)
132 require.ErrorIs(t, err, ErrPreambleRejected)
133 })
134
135 t.Run("deadline bounds silent peer", func(t *testing.T) {
136 client, _ := newProtoPipePair()
137 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
138 defer cancel()
139 _, err := runProtoClientPreamble(ctx, client, defaultProtoCeilings())
140 require.ErrorIs(t, err, ErrPreambleTimeout)
141 })
142
143 t.Run("oversize preamble rejected", func(t *testing.T) {
144 client, server := newProtoPipePair()
145 go func() {
146 _ = client.Send(wire.Envelope{Payload: make([]byte, wire.PreambleLimit+1)})
147 }()
148 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
149 defer cancel()
150 _, err := runProtoServerPreamble(ctx, server, defaultProtoCeilings())
151 require.ErrorIs(t, err, wire.ErrScanLength)
152 })
153}