test: add comprehensive tests for proto packages and wsstream

- Add rdsec_test.go covering all 4 message types (Identity, ClientInitPayload, SignedPayload, ServerInitPayload) - Round-trip MarshalVT/UnmarshalVT tests - CloneVT, EqualVT, SizeVT tests - Getter methods, Reset methods, and enum String/Enum tests - Coverage: 52.5% - Add rdverb_test.go covering all 11 message types (Packet, RelayInfo, Lease, etc.) - All packet type and response code enum tests - Getter methods and Reset methods - Complex nested message tests - Coverage: 36.6% - Add wsstream_test.go with comprehensive WebSocket stream wrapper tests - Mock implementation for isolated testing - Concurrent read/write safety tests - Multi-message streaming tests - Coverage: 93.1% - Refactor wsstream to use webSocketConn interface for testability - Update utils/ws.go to use wsstream.New() constructor

cognitive committed Dec 30, 2025 at 01:34 UTC ff7050ac728763c5d12d87cb36cefcfa114c4eac
5 files changed +2723 -18
portal/core/proto/rdsec/rdsec_test.go new
+925
@@ -0,0 +1,925 @@
1 +package rdsec
2 +
3 +import (
4 + "bytes"
5 + "testing"
6 +)
7 +
8 +// TestIdentity_MarshalVT_UnmarshalVT tests round-trip serialization for Identity
9 +func TestIdentity_MarshalVT_UnmarshalVT(t *testing.T) {
10 + tests := []struct {
11 + name string
12 + input *Identity
13 + wantErr bool
14 + }{
15 + {
16 + name: "empty",
17 + input: &Identity{},
18 + wantErr: false,
19 + },
20 + {
21 + name: "full",
22 + input: &Identity{
23 + Id: "test-id-12345",
24 + PublicKey: []byte{0x01, 0x02, 0x03, 0x04, 0x05},
25 + },
26 + wantErr: false,
27 + },
28 + {
29 + name: "id only",
30 + input: &Identity{
31 + Id: "client-id",
32 + },
33 + wantErr: false,
34 + },
35 + {
36 + name: "public key only",
37 + input: &Identity{
38 + PublicKey: []byte{0xAA, 0xBB, 0xCC, 0xDD},
39 + },
40 + wantErr: false,
41 + },
42 + }
43 +
44 + for _, tt := range tests {
45 + t.Run(tt.name, func(t *testing.T) {
46 + data, err := tt.input.MarshalVT()
47 + if (err != nil) != tt.wantErr {
48 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
49 + return
50 + }
51 +
52 + got := &Identity{}
53 + err = got.UnmarshalVT(data)
54 + if (err != nil) != tt.wantErr {
55 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
56 + return
57 + }
58 +
59 + if !tt.input.EqualVT(got) {
60 + t.Errorf("roundtrip mismatch: got %+v, want %+v", got, tt.input)
61 + }
62 + })
63 + }
64 +}
65 +
66 +// TestIdentity_CloneVT tests that CloneVT creates an independent copy
67 +func TestIdentity_CloneVT(t *testing.T) {
68 + original := &Identity{
69 + Id: "original-id",
70 + PublicKey: []byte{0x01, 0x02, 0x03},
71 + }
72 +
73 + cloned := original.CloneVT()
74 +
75 + // Verify clone equals original
76 + if !original.EqualVT(cloned) {
77 + t.Error("clone does not equal original")
78 + }
79 +
80 + // Modify clone
81 + cloned.Id = "modified-id"
82 + cloned.PublicKey[0] = 0xFF
83 +
84 + // Verify original unchanged
85 + if original.Id != "original-id" {
86 + t.Error("original.Id was modified")
87 + }
88 + if original.PublicKey[0] != 0x01 {
89 + t.Error("original.PublicKey was modified")
90 + }
91 +}
92 +
93 +// TestIdentity_EqualVT tests equality comparison
94 +func TestIdentity_EqualVT(t *testing.T) {
95 + tests := []struct {
96 + name string
97 + a *Identity
98 + b *Identity
99 + want bool
100 + }{
101 + {
102 + name: "both nil",
103 + a: nil,
104 + b: nil,
105 + want: true,
106 + },
107 + {
108 + name: "same instance",
109 + a: &Identity{Id: "test"},
110 + b: &Identity{Id: "test"},
111 + want: true,
112 + },
113 + {
114 + name: "different id",
115 + a: &Identity{Id: "test-a"},
116 + b: &Identity{Id: "test-b"},
117 + want: false,
118 + },
119 + {
120 + name: "different public key",
121 + a: &Identity{PublicKey: []byte{0x01}},
122 + b: &Identity{PublicKey: []byte{0x02}},
123 + want: false,
124 + },
125 + {
126 + name: "one nil",
127 + a: &Identity{Id: "test"},
128 + b: nil,
129 + want: false,
130 + },
131 + }
132 +
133 + for _, tt := range tests {
134 + t.Run(tt.name, func(t *testing.T) {
135 + if got := tt.a.EqualVT(tt.b); got != tt.want {
136 + t.Errorf("EqualVT() = %v, want %v", got, tt.want)
137 + }
138 + })
139 + }
140 +}
141 +
142 +// TestIdentity_SizeVT tests size calculation
143 +func TestIdentity_SizeVT(t *testing.T) {
144 + msg := &Identity{
145 + Id: "test-id",
146 + PublicKey: []byte{0x01, 0x02, 0x03},
147 + }
148 +
149 + size := msg.SizeVT()
150 + data, err := msg.MarshalVT()
151 + if err != nil {
152 + t.Fatalf("MarshalVT() error = %v", err)
153 + }
154 +
155 + if size != len(data) {
156 + t.Errorf("SizeVT() = %v, but MarshalVT() produced %v bytes", size, len(data))
157 + }
158 +}
159 +
160 +// TestClientInitPayload_MarshalVT_UnmarshalVT tests round-trip serialization
161 +func TestClientInitPayload_MarshalVT_UnmarshalVT(t *testing.T) {
162 + tests := []struct {
163 + name string
164 + input *ClientInitPayload
165 + wantErr bool
166 + }{
167 + {
168 + name: "empty",
169 + input: &ClientInitPayload{},
170 + wantErr: false,
171 + },
172 + {
173 + name: "full",
174 + input: &ClientInitPayload{
175 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
176 + Nonce: []byte{0x01, 0x02, 0x03, 0x04},
177 + Timestamp: 1234567890,
178 + Identity: &Identity{Id: "client-id", PublicKey: []byte{0xAA, 0xBB}},
179 + Alpn: "h2",
180 + SessionPublicKey: []byte{0x11, 0x22, 0x33, 0x44},
181 + },
182 + wantErr: false,
183 + },
184 + {
185 + name: "with identity only",
186 + input: &ClientInitPayload{
187 + Identity: &Identity{Id: "test-client"},
188 + },
189 + wantErr: false,
190 + },
191 + }
192 +
193 + for _, tt := range tests {
194 + t.Run(tt.name, func(t *testing.T) {
195 + data, err := tt.input.MarshalVT()
196 + if (err != nil) != tt.wantErr {
197 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
198 + return
199 + }
200 +
201 + got := &ClientInitPayload{}
202 + err = got.UnmarshalVT(data)
203 + if (err != nil) != tt.wantErr {
204 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
205 + return
206 + }
207 +
208 + if !tt.input.EqualVT(got) {
209 + t.Errorf("roundtrip mismatch")
210 + }
211 + })
212 + }
213 +}
214 +
215 +// TestClientInitPayload_CloneVT tests deep cloning with nested Identity
216 +func TestClientInitPayload_CloneVT(t *testing.T) {
217 + original := &ClientInitPayload{
218 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
219 + Nonce: []byte{0x01, 0x02},
220 + Timestamp: 999,
221 + Identity: &Identity{Id: "nested-id", PublicKey: []byte{0x03, 0x04}},
222 + Alpn: "h2",
223 + }
224 +
225 + cloned := original.CloneVT()
226 +
227 + // Verify clone equals original
228 + if !original.EqualVT(cloned) {
229 + t.Error("clone does not equal original")
230 + }
231 +
232 + // Modify nested identity in clone
233 + cloned.Identity.Id = "modified-nested"
234 + cloned.Identity.PublicKey[0] = 0xFF
235 +
236 + // Verify original nested identity unchanged
237 + if original.Identity.Id != "nested-id" {
238 + t.Error("original.Identity.Id was modified")
239 + }
240 + if original.Identity.PublicKey[0] != 0x03 {
241 + t.Error("original.Identity.PublicKey was modified")
242 + }
243 +}
244 +
245 +// TestSignedPayload_MarshalVT_UnmarshalVT tests round-trip serialization
246 +func TestSignedPayload_MarshalVT_UnmarshalVT(t *testing.T) {
247 + tests := []struct {
248 + name string
249 + input *SignedPayload
250 + wantErr bool
251 + }{
252 + {
253 + name: "empty",
254 + input: &SignedPayload{},
255 + wantErr: false,
256 + },
257 + {
258 + name: "full",
259 + input: &SignedPayload{
260 + Data: []byte{0x01, 0x02, 0x03, 0x04, 0x05},
261 + Signature: []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE},
262 + },
263 + wantErr: false,
264 + },
265 + {
266 + name: "data only",
267 + input: &SignedPayload{
268 + Data: []byte("payload data"),
269 + },
270 + wantErr: false,
271 + },
272 + {
273 + name: "signature only",
274 + input: &SignedPayload{
275 + Signature: []byte{0xFF, 0xFF, 0xFF},
276 + },
277 + wantErr: false,
278 + },
279 + }
280 +
281 + for _, tt := range tests {
282 + t.Run(tt.name, func(t *testing.T) {
283 + data, err := tt.input.MarshalVT()
284 + if (err != nil) != tt.wantErr {
285 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
286 + return
287 + }
288 +
289 + got := &SignedPayload{}
290 + err = got.UnmarshalVT(data)
291 + if (err != nil) != tt.wantErr {
292 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
293 + return
294 + }
295 +
296 + if !tt.input.EqualVT(got) {
297 + t.Errorf("roundtrip mismatch")
298 + }
299 + })
300 + }
301 +}
302 +
303 +// TestSignedPayload_CloneVT tests independent copy creation
304 +func TestSignedPayload_CloneVT(t *testing.T) {
305 + original := &SignedPayload{
306 + Data: []byte{0x01, 0x02, 0x03},
307 + Signature: []byte{0xAA, 0xBB, 0xCC},
308 + }
309 +
310 + cloned := original.CloneVT()
311 +
312 + // Modify clone
313 + cloned.Data[0] = 0xFF
314 + cloned.Signature[0] = 0x00
315 +
316 + // Verify original unchanged
317 + if original.Data[0] != 0x01 {
318 + t.Error("original.Data was modified")
319 + }
320 + if original.Signature[0] != 0xAA {
321 + t.Error("original.Signature was modified")
322 + }
323 +}
324 +
325 +// TestServerInitPayload_MarshalVT_UnmarshalVT tests round-trip serialization
326 +func TestServerInitPayload_MarshalVT_UnmarshalVT(t *testing.T) {
327 + tests := []struct {
328 + name string
329 + input *ServerInitPayload
330 + wantErr bool
331 + }{
332 + {
333 + name: "empty",
334 + input: &ServerInitPayload{},
335 + wantErr: false,
336 + },
337 + {
338 + name: "full",
339 + input: &ServerInitPayload{
340 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
341 + Nonce: []byte{0x01, 0x02, 0x03, 0x04},
342 + Timestamp: 9876543210,
343 + Identity: &Identity{Id: "server-id", PublicKey: []byte{0xAA, 0xBB}},
344 + Alpn: "h2",
345 + SessionPublicKey: []byte{0x11, 0x22, 0x33, 0x44},
346 + },
347 + wantErr: false,
348 + },
349 + {
350 + name: "with identity only",
351 + input: &ServerInitPayload{
352 + Identity: &Identity{Id: "test-server"},
353 + },
354 + wantErr: false,
355 + },
356 + }
357 +
358 + for _, tt := range tests {
359 + t.Run(tt.name, func(t *testing.T) {
360 + data, err := tt.input.MarshalVT()
361 + if (err != nil) != tt.wantErr {
362 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
363 + return
364 + }
365 +
366 + got := &ServerInitPayload{}
367 + err = got.UnmarshalVT(data)
368 + if (err != nil) != tt.wantErr {
369 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
370 + return
371 + }
372 +
373 + if !tt.input.EqualVT(got) {
374 + t.Errorf("roundtrip mismatch")
375 + }
376 + })
377 + }
378 +}
379 +
380 +// TestServerInitPayload_CloneVT tests deep cloning with nested Identity
381 +func TestServerInitPayload_CloneVT(t *testing.T) {
382 + original := &ServerInitPayload{
383 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
384 + Nonce: []byte{0x01, 0x02},
385 + Timestamp: 888,
386 + Identity: &Identity{Id: "server-nested", PublicKey: []byte{0x05, 0x06}},
387 + Alpn: "h2",
388 + }
389 +
390 + cloned := original.CloneVT()
391 +
392 + // Verify clone equals original
393 + if !original.EqualVT(cloned) {
394 + t.Error("clone does not equal original")
395 + }
396 +
397 + // Modify nested identity in clone
398 + cloned.Identity.Id = "modified-server"
399 +
400 + // Verify original nested identity unchanged
401 + if original.Identity.Id != "server-nested" {
402 + t.Error("original.Identity.Id was modified")
403 + }
404 +}
405 +
406 +// TestReset tests that Reset clears all fields
407 +func TestReset(t *testing.T) {
408 + // Test Identity reset
409 + ident := &Identity{
410 + Id: "test-id",
411 + PublicKey: []byte{0x01, 0x02},
412 + }
413 + ident.Reset()
414 + if ident.Id != "" {
415 + t.Error("Identity.Id not cleared after Reset()")
416 + }
417 + if ident.PublicKey != nil {
418 + t.Error("Identity.PublicKey not cleared after Reset()")
419 + }
420 +
421 + // Test ClientInitPayload reset
422 + payload := &ClientInitPayload{
423 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
424 + Nonce: []byte{0x01},
425 + Timestamp: 123,
426 + Identity: &Identity{Id: "test"},
427 + Alpn: "h2",
428 + }
429 + payload.Reset()
430 + if payload.Version != 0 {
431 + t.Error("ClientInitPayload.Version not cleared after Reset()")
432 + }
433 + if payload.Nonce != nil {
434 + t.Error("ClientInitPayload.Nonce not cleared after Reset()")
435 + }
436 + if payload.Timestamp != 0 {
437 + t.Error("ClientInitPayload.Timestamp not cleared after Reset()")
438 + }
439 + if payload.Identity != nil {
440 + t.Error("ClientInitPayload.Identity not cleared after Reset()")
441 + }
442 + if payload.Alpn != "" {
443 + t.Error("ClientInitPayload.Alpn not cleared after Reset()")
444 + }
445 +}
446 +
447 +// TestMarshalToSizedBufferVT tests buffer marshaling
448 +func TestMarshalToSizedBufferVT(t *testing.T) {
449 + msg := &Identity{
450 + Id: "buffer-test",
451 + PublicKey: []byte{0x01, 0x02, 0x03},
452 + }
453 +
454 + size := msg.SizeVT()
455 + buf := make([]byte, size)
456 +
457 + n, err := msg.MarshalToSizedBufferVT(buf)
458 + if err != nil {
459 + t.Fatalf("MarshalToSizedBufferVT() error = %v", err)
460 + }
461 +
462 + if n != size {
463 + t.Errorf("MarshalToSizedBufferVT() returned %v, want %v", n, size)
464 + }
465 +
466 + // Verify unmarshal works
467 + got := &Identity{}
468 + err = got.UnmarshalVT(buf[:n])
469 + if err != nil {
470 + t.Fatalf("UnmarshalVT() error = %v", err)
471 + }
472 +
473 + if !msg.EqualVT(got) {
474 + t.Error("roundtrip mismatch with MarshalToSizedBufferVT")
475 + }
476 +}
477 +
478 +// TestConcurrentSerialization tests concurrent marshal/unmarshal
479 +func TestConcurrentSerialization(t *testing.T) {
480 + msg := &ClientInitPayload{
481 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
482 + Nonce: []byte{0x01, 0x02, 0x03, 0x04},
483 + Timestamp: 1234567890,
484 + Identity: &Identity{Id: "concurrent-test", PublicKey: []byte{0xAA, 0xBB}},
485 + Alpn: "h2",
486 + SessionPublicKey: []byte{0x11, 0x22, 0x33, 0x44},
487 + }
488 +
489 + data, err := msg.MarshalVT()
490 + if err != nil {
491 + t.Fatalf("MarshalVT() error = %v", err)
492 + }
493 +
494 + // Run concurrent unmarshals
495 + done := make(chan bool, 10)
496 + for i := 0; i < 10; i++ {
497 + go func() {
498 + got := &ClientInitPayload{}
499 + if err := got.UnmarshalVT(data); err != nil {
500 + t.Errorf("concurrent UnmarshalVT() error = %v", err)
501 + }
502 + if !msg.EqualVT(got) {
503 + t.Error("concurrent roundtrip mismatch")
504 + }
505 + done <- true
506 + }()
507 + }
508 +
509 + for i := 0; i < 10; i++ {
510 + <-done
511 + }
512 +}
513 +
514 +// TestProtoMessage tests ProtoMessage stub exists
515 +func TestProtoMessage(t *testing.T) {
516 + // These tests just verify the stub methods exist and don't panic
517 + var (
518 + ident = &Identity{}
519 + clientInit = &ClientInitPayload{}
520 + signedPayload = &SignedPayload{}
521 + serverInit = &ServerInitPayload{}
522 + )
523 +
524 + // Should not panic
525 + ident.ProtoMessage()
526 + clientInit.ProtoMessage()
527 + signedPayload.ProtoMessage()
528 + serverInit.ProtoMessage()
529 +}
530 +
531 +// TestGetters tests getter methods
532 +func TestGetters(t *testing.T) {
533 + ident := &Identity{
534 + Id: "test-id",
535 + PublicKey: []byte{0x01, 0x02},
536 + }
537 +
538 + if got := ident.GetId(); got != "test-id" {
539 + t.Errorf("GetId() = %v, want test-id", got)
540 + }
541 + if got := ident.GetPublicKey(); len(got) != 2 || got[0] != 0x01 {
542 + t.Errorf("GetPublicKey() = %v, want [0x01, 0x02]", got)
543 + }
544 +
545 + // Test nil case
546 + var nilIdent *Identity
547 + if got := nilIdent.GetId(); got != "" {
548 + t.Errorf("GetId() on nil = %v, want empty string", got)
549 + }
550 + if got := nilIdent.GetPublicKey(); got != nil {
551 + t.Errorf("GetPublicKey() on nil = %v, want nil", got)
552 + }
553 +}
554 +
555 +// TestNilHandling tests nil message handling
556 +func TestNilHandling(t *testing.T) {
557 + var nilIdent *Identity
558 +
559 + // MarshalVT on nil should return nil, nil
560 + if data, err := nilIdent.MarshalVT(); err != nil || data != nil {
561 + t.Errorf("MarshalVT() on nil = (%v, %v), want (nil, nil)", data, err)
562 + }
563 +
564 + // CloneVT on nil should return nil
565 + if cloned := nilIdent.CloneVT(); cloned != nil {
566 + t.Errorf("CloneVT() on nil = %v, want nil", cloned)
567 + }
568 +
569 + // SizeVT on nil should return 0
570 + if size := nilIdent.SizeVT(); size != 0 {
571 + t.Errorf("SizeVT() on nil = %v, want 0", size)
572 + }
573 +
574 + // EqualVT on nil with nil should return true
575 + if !nilIdent.EqualVT(nil) {
576 + t.Error("EqualVT(nil, nil) = false, want true")
577 + }
578 +
579 + // EqualVT on nil with non-nil should return false
580 + if nilIdent.EqualVT(&Identity{}) {
581 + t.Error("EqualVT(nil, &Identity{}) = true, want false")
582 + }
583 +
584 + // Test all message types handle nil correctly
585 + testCases := []struct {
586 + name string
587 + test func() // test function that verifies nil handling
588 + }{
589 + {"ClientInitPayload", func() {
590 + var msg *ClientInitPayload
591 + if data, err := msg.MarshalVT(); err != nil || data != nil {
592 + t.Errorf("MarshalVT() on nil ClientInitPayload = (%v, %v), want (nil, nil)", data, err)
593 + }
594 + if msg.CloneVT() != nil {
595 + t.Error("CloneVT() on nil ClientInitPayload should return nil")
596 + }
597 + if msg.SizeVT() != 0 {
598 + t.Error("SizeVT() on nil ClientInitPayload should return 0")
599 + }
600 + }},
601 + {"SignedPayload", func() {
602 + var msg *SignedPayload
603 + if data, err := msg.MarshalVT(); err != nil || data != nil {
604 + t.Errorf("MarshalVT() on nil SignedPayload = (%v, %v), want (nil, nil)", data, err)
605 + }
606 + if msg.CloneVT() != nil {
607 + t.Error("CloneVT() on nil SignedPayload should return nil")
608 + }
609 + }},
610 + {"ServerInitPayload", func() {
611 + var msg *ServerInitPayload
612 + if data, err := msg.MarshalVT(); err != nil || data != nil {
613 + t.Errorf("MarshalVT() on nil ServerInitPayload = (%v, %v), want (nil, nil)", data, err)
614 + }
615 + if msg.CloneVT() != nil {
616 + t.Error("CloneVT() on nil ServerInitPayload should return nil")
617 + }
618 + }},
619 + }
620 +
621 + for _, tc := range testCases {
622 + t.Run(tc.name, func(t *testing.T) {
623 + tc.test()
624 + })
625 + }
626 +}
627 +
628 +// TestMarshalVTStrict tests strict marshaling
629 +func TestMarshalVTStrict(t *testing.T) {
630 + msg := &Identity{
631 + Id: "strict-test",
632 + PublicKey: []byte{0x01, 0x02, 0x03},
633 + }
634 +
635 + data, err := msg.MarshalVTStrict()
636 + if err != nil {
637 + t.Fatalf("MarshalVTStrict() error = %v", err)
638 + }
639 +
640 + got := &Identity{}
641 + err = got.UnmarshalVT(data)
642 + if err != nil {
643 + t.Fatalf("UnmarshalVT() error = %v", err)
644 + }
645 +
646 + if !msg.EqualVT(got) {
647 + t.Error("MarshalVTStrict roundtrip mismatch")
648 + }
649 +}
650 +
651 +// TestUnmarshalVTUnsafe tests unsafe unmarshaling
652 +func TestUnmarshalVTUnsafe(t *testing.T) {
653 + msg := &ClientInitPayload{
654 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
655 + Nonce: []byte{0x01, 0x02, 0x03, 0x04},
656 + Timestamp: 1234567890,
657 + Identity: &Identity{Id: "unsafe-test", PublicKey: []byte{0xAA, 0xBB}},
658 + Alpn: "h2",
659 + SessionPublicKey: []byte{0x11, 0x22, 0x33, 0x44},
660 + }
661 +
662 + data, err := msg.MarshalVT()
663 + if err != nil {
664 + t.Fatalf("MarshalVT() error = %v", err)
665 + }
666 +
667 + got := &ClientInitPayload{}
668 + err = got.UnmarshalVTUnsafe(data)
669 + if err != nil {
670 + t.Fatalf("UnmarshalVTUnsafe() error = %v", err)
671 + }
672 +
673 + if !msg.EqualVT(got) {
674 + t.Error("UnmarshalVTUnsafe roundtrip mismatch")
675 + }
676 +}
677 +
678 +// BenchmarkIdentity_MarshalVT benchmarks marshaling
679 +func BenchmarkIdentity_MarshalVT(b *testing.B) {
680 + msg := &Identity{
681 + Id: "benchmark-test-id-12345",
682 + PublicKey: bytes.Repeat([]byte{0xAA}, 32),
683 + }
684 +
685 + b.ResetTimer()
686 + for i := 0; i < b.N; i++ {
687 + _, _ = msg.MarshalVT()
688 + }
689 +}
690 +
691 +// BenchmarkIdentity_UnmarshalVT benchmarks unmarshaling
692 +func BenchmarkIdentity_UnmarshalVT(b *testing.B) {
693 + msg := &Identity{
694 + Id: "benchmark-test-id-12345",
695 + PublicKey: bytes.Repeat([]byte{0xAA}, 32),
696 + }
697 +
698 + data, _ := msg.MarshalVT()
699 +
700 + b.ResetTimer()
701 + for i := 0; i < b.N; i++ {
702 + got := &Identity{}
703 + _ = got.UnmarshalVT(data)
704 + }
705 +}
706 +
707 +// BenchmarkClientInitPayload_MarshalVT benchmarks complex message marshaling
708 +func BenchmarkClientInitPayload_MarshalVT(b *testing.B) {
709 + msg := &ClientInitPayload{
710 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
711 + Nonce: bytes.Repeat([]byte{0x01}, 32),
712 + Timestamp: 1234567890,
713 + Identity: &Identity{Id: "benchmark-client", PublicKey: bytes.Repeat([]byte{0xAA}, 32)},
714 + Alpn: "h2",
715 + SessionPublicKey: bytes.Repeat([]byte{0xFF}, 32),
716 + }
717 +
718 + b.ResetTimer()
719 + for i := 0; i < b.N; i++ {
720 + _, _ = msg.MarshalVT()
721 + }
722 +}
723 +
724 +// BenchmarkClientInitPayload_UnmarshalVT benchmarks complex message unmarshaling
725 +func BenchmarkClientInitPayload_UnmarshalVT(b *testing.B) {
726 + msg := &ClientInitPayload{
727 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
728 + Nonce: bytes.Repeat([]byte{0x01}, 32),
729 + Timestamp: 1234567890,
730 + Identity: &Identity{Id: "benchmark-client", PublicKey: bytes.Repeat([]byte{0xAA}, 32)},
731 + Alpn: "h2",
732 + SessionPublicKey: bytes.Repeat([]byte{0xFF}, 32),
733 + }
734 +
735 + data, _ := msg.MarshalVT()
736 +
737 + b.ResetTimer()
738 + for i := 0; i < b.N; i++ {
739 + got := &ClientInitPayload{}
740 + _ = got.UnmarshalVT(data)
741 + }
742 +}
743 +
744 +// TestProtocolVersion_String tests enum String method
745 +func TestProtocolVersion_String(t *testing.T) {
746 + tests := []struct {
747 + name string
748 + enum ProtocolVersion
749 + want string
750 + }{
751 + {"PROTOCOL_VERSION_1", ProtocolVersion_PROTOCOL_VERSION_1, "PROTOCOL_VERSION_1"},
752 + }
753 +
754 + for _, tt := range tests {
755 + t.Run(tt.name, func(t *testing.T) {
756 + if got := tt.enum.String(); got != tt.want {
757 + t.Errorf("ProtocolVersion.String() = %v, want %v", got, tt.want)
758 + }
759 + })
760 + }
761 +
762 + // Test that invalid value returns a non-empty string
763 + invalid := ProtocolVersion(999).String()
764 + if invalid == "" {
765 + t.Error("ProtocolVersion(999).String() should return non-empty string")
766 + }
767 +}
768 +
769 +// TestProtocolVersion_Enum tests Enum method
770 +func TestProtocolVersion_Enum(t *testing.T) {
771 + if ProtocolVersion_PROTOCOL_VERSION_1.Enum() != nil && *ProtocolVersion_PROTOCOL_VERSION_1.Enum() != 0 {
772 + t.Error("ProtocolVersion.Enum() should return 0")
773 + }
774 +}
775 +
776 +// TestClientInitPayload_Getters tests all getter methods
777 +func TestClientInitPayload_Getters(t *testing.T) {
778 + msg := &ClientInitPayload{
779 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
780 + Nonce: []byte{0x01, 0x02, 0x03},
781 + Timestamp: 1234567890,
782 + Identity: &Identity{Id: "getter-test", PublicKey: []byte{0xAA}},
783 + Alpn: "h2",
784 + SessionPublicKey: []byte{0x11, 0x22},
785 + }
786 +
787 + if got := msg.GetVersion(); got != ProtocolVersion_PROTOCOL_VERSION_1 {
788 + t.Errorf("GetVersion() = %v, want %v", got, ProtocolVersion_PROTOCOL_VERSION_1)
789 + }
790 + if got := msg.GetNonce(); !bytes.Equal(got, []byte{0x01, 0x02, 0x03}) {
791 + t.Errorf("GetNonce() = %v, want [1 2 3]", got)
792 + }
793 + if got := msg.GetTimestamp(); got != 1234567890 {
794 + t.Errorf("GetTimestamp() = %v, want 1234567890", got)
795 + }
796 + if got := msg.GetIdentity(); got == nil || got.Id != "getter-test" {
797 + t.Errorf("GetIdentity() = %v, want Id='getter-test'", got)
798 + }
799 + if got := msg.GetAlpn(); got != "h2" {
800 + t.Errorf("GetAlpn() = %v, want h2", got)
801 + }
802 + if got := msg.GetSessionPublicKey(); !bytes.Equal(got, []byte{0x11, 0x22}) {
803 + t.Errorf("GetSessionPublicKey() = %v, want [17 34]", got)
804 + }
805 +
806 + // Test nil defaults
807 + empty := &ClientInitPayload{}
808 + if got := empty.GetVersion(); got != ProtocolVersion_PROTOCOL_VERSION_1 {
809 + t.Errorf("empty GetVersion() should return default")
810 + }
811 + if got := empty.GetNonce(); got != nil {
812 + t.Errorf("empty GetNonce() = %v, want nil", got)
813 + }
814 + if got := empty.GetIdentity(); got != nil {
815 + t.Errorf("empty GetIdentity() = %v, want nil", got)
816 + }
817 + if got := empty.GetAlpn(); got != "" {
818 + t.Errorf("empty GetAlpn() = %v, want empty string", got)
819 + }
820 +}
821 +
822 +// TestSignedPayload_Getters tests all getter methods
823 +func TestSignedPayload_Getters(t *testing.T) {
824 + msg := &SignedPayload{
825 + Data: []byte{0x01, 0x02, 0x03},
826 + Signature: []byte{0xAA, 0xBB},
827 + }
828 +
829 + if got := msg.GetData(); !bytes.Equal(got, []byte{0x01, 0x02, 0x03}) {
830 + t.Errorf("GetData() = %v, want [1 2 3]", got)
831 + }
832 + if got := msg.GetSignature(); !bytes.Equal(got, []byte{0xAA, 0xBB}) {
833 + t.Errorf("GetSignature() = %v, want [170 187]", got)
834 + }
835 +
836 + // Test nil defaults
837 + empty := &SignedPayload{}
838 + if got := empty.GetData(); got != nil {
839 + t.Errorf("empty GetData() = %v, want nil", got)
840 + }
841 + if got := empty.GetSignature(); got != nil {
842 + t.Errorf("empty GetSignature() = %v, want nil", got)
843 + }
844 +}
845 +
846 +// TestServerInitPayload_Getters tests all getter methods
847 +func TestServerInitPayload_Getters(t *testing.T) {
848 + msg := &ServerInitPayload{
849 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
850 + Nonce: []byte{0x01, 0x02},
851 + Timestamp: 9876543210,
852 + Identity: &Identity{Id: "server-test", PublicKey: []byte{}},
853 + Alpn: "h3",
854 + SessionPublicKey: []byte{0xCC, 0xDD},
855 + }
856 +
857 + if got := msg.GetVersion(); got != ProtocolVersion_PROTOCOL_VERSION_1 {
858 + t.Errorf("GetVersion() = %v, want %v", got, ProtocolVersion_PROTOCOL_VERSION_1)
859 + }
860 + if got := msg.GetNonce(); !bytes.Equal(got, []byte{0x01, 0x02}) {
861 + t.Errorf("GetNonce() = %v, want [1 2]", got)
862 + }
863 + if got := msg.GetTimestamp(); got != 9876543210 {
864 + t.Errorf("GetTimestamp() = %v, want 9876543210", got)
865 + }
866 + if got := msg.GetIdentity(); got == nil || got.Id != "server-test" {
867 + t.Errorf("GetIdentity() = %v, want Id='server-test'", got)
868 + }
869 + if got := msg.GetAlpn(); got != "h3" {
870 + t.Errorf("GetAlpn() = %v, want h3", got)
871 + }
872 + if got := msg.GetSessionPublicKey(); !bytes.Equal(got, []byte{0xCC, 0xDD}) {
873 + t.Errorf("GetSessionPublicKey() = %v, want [204 221]", got)
874 + }
875 +}
876 +
877 +// TestSignedPayload_Reset tests Reset method
878 +func TestSignedPayload_Reset(t *testing.T) {
879 + msg := &SignedPayload{
880 + Data: []byte{0x01, 0x02},
881 + Signature: []byte{0xAA, 0xBB},
882 + }
883 +
884 + msg.Reset()
885 +
886 + if msg.Data != nil {
887 + t.Error("Reset() did not clear Data")
888 + }
889 + if msg.Signature != nil {
890 + t.Error("Reset() did not clear Signature")
891 + }
892 +}
893 +
894 +// TestServerInitPayload_Reset tests Reset method
895 +func TestServerInitPayload_Reset(t *testing.T) {
896 + msg := &ServerInitPayload{
897 + Version: ProtocolVersion_PROTOCOL_VERSION_1,
898 + Nonce: []byte{0x01},
899 + Timestamp: 123,
900 + Identity: &Identity{Id: "test"},
901 + Alpn: "h2",
902 + SessionPublicKey: []byte{0xAA},
903 + }
904 +
905 + msg.Reset()
906 +
907 + if msg.Version != ProtocolVersion_PROTOCOL_VERSION_1 {
908 + t.Error("Reset() changed Version from default")
909 + }
910 + if msg.Nonce != nil {
911 + t.Error("Reset() did not clear Nonce")
912 + }
913 + if msg.Timestamp != 0 {
914 + t.Error("Reset() did not clear Timestamp")
915 + }
916 + if msg.Identity != nil {
917 + t.Error("Reset() did not clear Identity")
918 + }
919 + if msg.Alpn != "" {
920 + t.Error("Reset() did not clear Alpn")
921 + }
922 + if msg.SessionPublicKey != nil {
923 + t.Error("Reset() did not clear SessionPublicKey")
924 + }
925 +}
portal/core/proto/rdverb/rdverb_test.go new
+1300
@@ -0,0 +1,1300 @@
1 +package rdverb
2 +
3 +import (
4 + "bytes"
5 + "testing"
6 +
7 + rdsec "gosuda.org/portal/portal/core/proto/rdsec"
8 +)
9 +
10 +// TestPacket_MarshalVT_UnmarshalVT tests round-trip serialization for Packet
11 +func TestPacket_MarshalVT_UnmarshalVT(t *testing.T) {
12 + tests := []struct {
13 + name string
14 + input *Packet
15 + wantErr bool
16 + }{
17 + {
18 + name: "empty",
19 + input: &Packet{},
20 + wantErr: false,
21 + },
22 + {
23 + name: "full",
24 + input: &Packet{
25 + Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST,
26 + Payload: []byte{0x01, 0x02, 0x03, 0x04},
27 + },
28 + wantErr: false,
29 + },
30 + {
31 + name: "type only",
32 + input: &Packet{
33 + Type: PacketType_PACKET_TYPE_LEASE_UPDATE_REQUEST,
34 + },
35 + wantErr: false,
36 + },
37 + {
38 + name: "payload only",
39 + input: &Packet{
40 + Payload: []byte("test payload"),
41 + },
42 + wantErr: false,
43 + },
44 + }
45 +
46 + for _, tt := range tests {
47 + t.Run(tt.name, func(t *testing.T) {
48 + data, err := tt.input.MarshalVT()
49 + if (err != nil) != tt.wantErr {
50 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
51 + return
52 + }
53 +
54 + got := &Packet{}
55 + err = got.UnmarshalVT(data)
56 + if (err != nil) != tt.wantErr {
57 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
58 + return
59 + }
60 +
61 + if !tt.input.EqualVT(got) {
62 + t.Errorf("roundtrip mismatch")
63 + }
64 + })
65 + }
66 +}
67 +
68 +// TestPacket_AllPacketTypes tests serialization of all packet types
69 +func TestPacket_AllPacketTypes(t *testing.T) {
70 + packetTypes := []PacketType{
71 + PacketType_PACKET_TYPE_RELAY_INFO_REQUEST,
72 + PacketType_PACKET_TYPE_RELAY_INFO_RESPONSE,
73 + PacketType_PACKET_TYPE_LEASE_UPDATE_REQUEST,
74 + PacketType_PACKET_TYPE_LEASE_UPDATE_RESPONSE,
75 + PacketType_PACKET_TYPE_LEASE_DELETE_REQUEST,
76 + PacketType_PACKET_TYPE_LEASE_DELETE_RESPONSE,
77 + PacketType_PACKET_TYPE_CONNECTION_REQUEST,
78 + PacketType_PACKET_TYPE_CONNECTION_RESPONSE,
79 + }
80 +
81 + for _, pt := range packetTypes {
82 + t.Run(pt.String(), func(t *testing.T) {
83 + msg := &Packet{
84 + Type: pt,
85 + Payload: []byte("test payload"),
86 + }
87 +
88 + data, err := msg.MarshalVT()
89 + if err != nil {
90 + t.Fatalf("MarshalVT() error = %v", err)
91 + }
92 +
93 + got := &Packet{}
94 + if err := got.UnmarshalVT(data); err != nil {
95 + t.Fatalf("UnmarshalVT() error = %v", err)
96 + }
97 +
98 + if !msg.EqualVT(got) {
99 + t.Errorf("roundtrip mismatch for %v", pt)
100 + }
101 + })
102 + }
103 +}
104 +
105 +// TestRelayInfo_MarshalVT_UnmarshalVT tests round-trip serialization
106 +func TestRelayInfo_MarshalVT_UnmarshalVT(t *testing.T) {
107 + tests := []struct {
108 + name string
109 + input *RelayInfo
110 + wantErr bool
111 + }{
112 + {
113 + name: "empty",
114 + input: &RelayInfo{},
115 + wantErr: false,
116 + },
117 + {
118 + name: "full",
119 + input: &RelayInfo{
120 + Identity: &rdsec.Identity{
121 + Id: "relay-id",
122 + PublicKey: []byte{0x01, 0x02},
123 + },
124 + Address: []string{"addr1.example.com:8080", "addr2.example.com:8080"},
125 + Leases: []*Lease{
126 + {
127 + Identity: &rdsec.Identity{Id: "lease1-id"},
128 + Expires: 1234567890,
129 + Name: "lease1",
130 + Alpn: []string{"h2", "http/1.1"},
131 + Metadata: "metadata1",
132 + },
133 + {
134 + Identity: &rdsec.Identity{Id: "lease2-id"},
135 + Expires: 9876543210,
136 + Name: "lease2",
137 + Alpn: []string{"h2"},
138 + },
139 + },
140 + },
141 + wantErr: false,
142 + },
143 + {
144 + name: "with identity only",
145 + input: &RelayInfo{
146 + Identity: &rdsec.Identity{Id: "test-relay"},
147 + },
148 + wantErr: false,
149 + },
150 + {
151 + name: "with multiple addresses",
152 + input: &RelayInfo{
153 + Identity: &rdsec.Identity{Id: "multi-addr"},
154 + Address: []string{"addr1:8080", "addr2:8080", "addr3:8080"},
155 + },
156 + wantErr: false,
157 + },
158 + }
159 +
160 + for _, tt := range tests {
161 + t.Run(tt.name, func(t *testing.T) {
162 + data, err := tt.input.MarshalVT()
163 + if (err != nil) != tt.wantErr {
164 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
165 + return
166 + }
167 +
168 + got := &RelayInfo{}
169 + err = got.UnmarshalVT(data)
170 + if (err != nil) != tt.wantErr {
171 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
172 + return
173 + }
174 +
175 + if !tt.input.EqualVT(got) {
176 + t.Errorf("roundtrip mismatch")
177 + }
178 + })
179 + }
180 +}
181 +
182 +// TestRelayInfo_WithMultipleLeases tests array handling for leases
183 +func TestRelayInfo_WithMultipleLeases(t *testing.T) {
184 + leases := []*Lease{
185 + {Identity: &rdsec.Identity{Id: "l1"}, Expires: 100, Name: "lease1"},
186 + {Identity: &rdsec.Identity{Id: "l2"}, Expires: 200, Name: "lease2"},
187 + {Identity: &rdsec.Identity{Id: "l3"}, Expires: 300, Name: "lease3"},
188 + {Identity: &rdsec.Identity{Id: "l4"}, Expires: 400, Name: "lease4"},
189 + {Identity: &rdsec.Identity{Id: "l5"}, Expires: 500, Name: "lease5"},
190 + }
191 +
192 + msg := &RelayInfo{
193 + Identity: &rdsec.Identity{Id: "relay"},
194 + Leases: leases,
195 + }
196 +
197 + data, err := msg.MarshalVT()
198 + if err != nil {
199 + t.Fatalf("MarshalVT() error = %v", err)
200 + }
201 +
202 + got := &RelayInfo{}
203 + if err := got.UnmarshalVT(data); err != nil {
204 + t.Fatalf("UnmarshalVT() error = %v", err)
205 + }
206 +
207 + if len(got.Leases) != len(leases) {
208 + t.Fatalf("got %d leases, want %d", len(got.Leases), len(leases))
209 + }
210 +
211 + for i, want := range leases {
212 + if got.Leases[i].Name != want.Name {
213 + t.Errorf("lease[%d].Name = %v, want %v", i, got.Leases[i].Name, want.Name)
214 + }
215 + }
216 +}
217 +
218 +// TestLease_MarshalVT_UnmarshalVT tests round-trip serialization
219 +func TestLease_MarshalVT_UnmarshalVT(t *testing.T) {
220 + tests := []struct {
221 + name string
222 + input *Lease
223 + wantErr bool
224 + }{
225 + {
226 + name: "empty",
227 + input: &Lease{},
228 + wantErr: false,
229 + },
230 + {
231 + name: "full",
232 + input: &Lease{
233 + Identity: &rdsec.Identity{
234 + Id: "lease-id",
235 + PublicKey: []byte{0x01, 0x02},
236 + },
237 + Expires: 1234567890,
238 + Name: "my-lease",
239 + Alpn: []string{"h2", "http/1.1"},
240 + Metadata: "some metadata",
241 + },
242 + wantErr: false,
243 + },
244 + {
245 + name: "with alpn",
246 + input: &Lease{
247 + Identity: &rdsec.Identity{Id: "alpn-test"},
248 + Alpn: []string{"h2", "grpc"},
249 + },
250 + wantErr: false,
251 + },
252 + }
253 +
254 + for _, tt := range tests {
255 + t.Run(tt.name, func(t *testing.T) {
256 + data, err := tt.input.MarshalVT()
257 + if (err != nil) != tt.wantErr {
258 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
259 + return
260 + }
261 +
262 + got := &Lease{}
263 + err = got.UnmarshalVT(data)
264 + if (err != nil) != tt.wantErr {
265 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
266 + return
267 + }
268 +
269 + if !tt.input.EqualVT(got) {
270 + t.Errorf("roundtrip mismatch")
271 + }
272 + })
273 + }
274 +}
275 +
276 +// TestLeaseUpdateRequest_MarshalVT_UnmarshalVT tests round-trip serialization
277 +func TestLeaseUpdateRequest_MarshalVT_UnmarshalVT(t *testing.T) {
278 + lease := &Lease{
279 + Identity: &rdsec.Identity{Id: "update-lease"},
280 + Expires: 1234567890,
281 + Name: "lease-name",
282 + }
283 +
284 + tests := []struct {
285 + name string
286 + input *LeaseUpdateRequest
287 + wantErr bool
288 + }{
289 + {
290 + name: "full",
291 + input: &LeaseUpdateRequest{
292 + Lease: lease,
293 + Nonce: []byte{0x01, 0x02, 0x03, 0x04},
294 + Timestamp: 9876543210,
295 + },
296 + wantErr: false,
297 + },
298 + {
299 + name: "empty",
300 + input: &LeaseUpdateRequest{},
301 + wantErr: false,
302 + },
303 + }
304 +
305 + for _, tt := range tests {
306 + t.Run(tt.name, func(t *testing.T) {
307 + data, err := tt.input.MarshalVT()
308 + if (err != nil) != tt.wantErr {
309 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
310 + return
311 + }
312 +
313 + got := &LeaseUpdateRequest{}
314 + err = got.UnmarshalVT(data)
315 + if (err != nil) != tt.wantErr {
316 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
317 + return
318 + }
319 +
320 + if !tt.input.EqualVT(got) {
321 + t.Errorf("roundtrip mismatch")
322 + }
323 + })
324 + }
325 +}
326 +
327 +// TestResponseCode_AllValues tests all response code values
328 +func TestResponseCode_AllValues(t *testing.T) {
329 + codes := []ResponseCode{
330 + ResponseCode_RESPONSE_CODE_UNKNOWN,
331 + ResponseCode_RESPONSE_CODE_ACCEPTED,
332 + ResponseCode_RESPONSE_CODE_INVALID_EXPIRES,
333 + ResponseCode_RESPONSE_CODE_INVALID_IDENTITY,
334 + ResponseCode_RESPONSE_CODE_INVALID_NAME,
335 + ResponseCode_RESPONSE_CODE_INVALID_ALPN,
336 + ResponseCode_RESPONSE_CODE_REJECTED,
337 + }
338 +
339 + for _, code := range codes {
340 + t.Run(code.String(), func(t *testing.T) {
341 + msg := &LeaseUpdateResponse{Code: code}
342 +
343 + data, err := msg.MarshalVT()
344 + if err != nil {
345 + t.Fatalf("MarshalVT() error = %v", err)
346 + }
347 +
348 + got := &LeaseUpdateResponse{}
349 + if err := got.UnmarshalVT(data); err != nil {
350 + t.Fatalf("UnmarshalVT() error = %v", err)
351 + }
352 +
353 + if got.Code != code {
354 + t.Errorf("Code = %v, want %v", got.Code, code)
355 + }
356 + })
357 + }
358 +}
359 +
360 +// TestLeaseDeleteRequest_MarshalVT_UnmarshalVT tests round-trip serialization
361 +func TestLeaseDeleteRequest_MarshalVT_UnmarshalVT(t *testing.T) {
362 + tests := []struct {
363 + name string
364 + input *LeaseDeleteRequest
365 + wantErr bool
366 + }{
367 + {
368 + name: "full",
369 + input: &LeaseDeleteRequest{
370 + Identity: &rdsec.Identity{Id: "delete-id", PublicKey: []byte{0x01}},
371 + Nonce: []byte{0x01, 0x02, 0x03},
372 + Timestamp: 1234567890,
373 + },
374 + wantErr: false,
375 + },
376 + {
377 + name: "empty",
378 + input: &LeaseDeleteRequest{},
379 + wantErr: false,
380 + },
381 + }
382 +
383 + for _, tt := range tests {
384 + t.Run(tt.name, func(t *testing.T) {
385 + data, err := tt.input.MarshalVT()
386 + if (err != nil) != tt.wantErr {
387 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
388 + return
389 + }
390 +
391 + got := &LeaseDeleteRequest{}
392 + err = got.UnmarshalVT(data)
393 + if (err != nil) != tt.wantErr {
394 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
395 + return
396 + }
397 +
398 + if !tt.input.EqualVT(got) {
399 + t.Errorf("roundtrip mismatch")
400 + }
401 + })
402 + }
403 +}
404 +
405 +// TestConnectionRequest_MarshalVT_UnmarshalVT tests round-trip serialization
406 +func TestConnectionRequest_MarshalVT_UnmarshalVT(t *testing.T) {
407 + tests := []struct {
408 + name string
409 + input *ConnectionRequest
410 + wantErr bool
411 + }{
412 + {
413 + name: "full",
414 + input: &ConnectionRequest{
415 + LeaseId: "lease-123",
416 + ClientIdentity: &rdsec.Identity{Id: "client-id", PublicKey: []byte{0xAA, 0xBB}},
417 + },
418 + wantErr: false,
419 + },
420 + {
421 + name: "empty",
422 + input: &ConnectionRequest{},
423 + wantErr: false,
424 + },
425 + {
426 + name: "lease id only",
427 + input: &ConnectionRequest{
428 + LeaseId: "lease-only",
429 + },
430 + wantErr: false,
431 + },
432 + }
433 +
434 + for _, tt := range tests {
435 + t.Run(tt.name, func(t *testing.T) {
436 + data, err := tt.input.MarshalVT()
437 + if (err != nil) != tt.wantErr {
438 + t.Errorf("MarshalVT() error = %v, wantErr %v", err, tt.wantErr)
439 + return
440 + }
441 +
442 + got := &ConnectionRequest{}
443 + err = got.UnmarshalVT(data)
444 + if (err != nil) != tt.wantErr {
445 + t.Errorf("UnmarshalVT() error = %v, wantErr %v", err, tt.wantErr)
446 + return
447 + }
448 +
449 + if !tt.input.EqualVT(got) {
450 + t.Errorf("roundtrip mismatch")
451 + }
452 + })
453 + }
454 +}
455 +
456 +// TestRelayInfoRequest_MarshalVT_UnmarshalVT tests empty message
457 +func TestRelayInfoRequest_MarshalVT_UnmarshalVT(t *testing.T) {
458 + msg := &RelayInfoRequest{}
459 +
460 + data, err := msg.MarshalVT()
461 + if err != nil {
462 + t.Fatalf("MarshalVT() error = %v", err)
463 + }
464 +
465 + got := &RelayInfoRequest{}
466 + err = got.UnmarshalVT(data)
467 + if err != nil {
468 + t.Fatalf("UnmarshalVT() error = %v", err)
469 + }
470 +
471 + if !msg.EqualVT(got) {
472 + t.Error("roundtrip mismatch for empty RelayInfoRequest")
473 + }
474 +}
475 +
476 +// TestRelayInfoResponse_MarshalVT_UnmarshalVT tests with RelayInfo
477 +func TestRelayInfoResponse_MarshalVT_UnmarshalVT(t *testing.T) {
478 + relayInfo := &RelayInfo{
479 + Identity: &rdsec.Identity{Id: "response-relay"},
480 + Address: []string{"addr1:8080"},
481 + Leases: []*Lease{
482 + {Identity: &rdsec.Identity{Id: "l1"}, Name: "lease1"},
483 + },
484 + }
485 +
486 + msg := &RelayInfoResponse{RelayInfo: relayInfo}
487 +
488 + data, err := msg.MarshalVT()
489 + if err != nil {
490 + t.Fatalf("MarshalVT() error = %v", err)
491 + }
492 +
493 + got := &RelayInfoResponse{}
494 + err = got.UnmarshalVT(data)
495 + if err != nil {
496 + t.Fatalf("UnmarshalVT() error = %v", err)
497 + }
498 +
499 + if !msg.EqualVT(got) {
500 + t.Error("roundtrip mismatch")
501 + }
502 +}
503 +
504 +// TestCloneVT tests cloning creates independent copies
505 +func TestCloneVT(t *testing.T) {
506 + t.Run("Packet", func(t *testing.T) {
507 + original := &Packet{
508 + Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST,
509 + Payload: []byte{0x01, 0x02, 0x03},
510 + }
511 + cloned := original.CloneVT()
512 +
513 + cloned.Type = PacketType_PACKET_TYPE_LEASE_UPDATE_REQUEST
514 + cloned.Payload[0] = 0xFF
515 +
516 + if original.Type != PacketType_PACKET_TYPE_CONNECTION_REQUEST {
517 + t.Error("original.Type was modified")
518 + }
519 + if original.Payload[0] != 0x01 {
520 + t.Error("original.Payload was modified")
521 + }
522 + })
523 +
524 + t.Run("RelayInfo", func(t *testing.T) {
525 + original := &RelayInfo{
526 + Identity: &rdsec.Identity{Id: "test"},
527 + Address: []string{"addr1"},
528 + Leases: []*Lease{
529 + {Identity: &rdsec.Identity{Id: "l1"}, Name: "lease1"},
530 + },
531 + }
532 + cloned := original.CloneVT()
533 +
534 + cloned.Identity.Id = "modified"
535 + cloned.Address[0] = "modified-addr"
536 + cloned.Leases[0].Name = "modified-lease"
537 +
538 + if original.Identity.Id != "test" {
539 + t.Error("original.Identity.Id was modified")
540 + }
541 + if original.Address[0] != "addr1" {
542 + t.Error("original.Address was modified")
543 + }
544 + if original.Leases[0].Name != "lease1" {
545 + t.Error("original.Leases[0].Name was modified")
546 + }
547 + })
548 +
549 + t.Run("Lease", func(t *testing.T) {
550 + original := &Lease{
551 + Identity: &rdsec.Identity{Id: "lease-clone"},
552 + Expires: 12345,
553 + Name: "clone-lease",
554 + Alpn: []string{"h2"},
555 + }
556 + cloned := original.CloneVT()
557 +
558 + cloned.Identity.Id = "modified"
559 + cloned.Expires = 99999
560 + cloned.Name = "modified"
561 + cloned.Alpn[0] = "modified"
562 +
563 + if original.Identity.Id != "lease-clone" {
564 + t.Error("original.Identity.Id was modified")
565 + }
566 + if original.Expires != 12345 {
567 + t.Error("original.Expires was modified")
568 + }
569 + if original.Name != "clone-lease" {
570 + t.Error("original.Name was modified")
571 + }
572 + if original.Alpn[0] != "h2" {
573 + t.Error("original.Alpn was modified")
574 + }
575 + })
576 +}
577 +
578 +// TestEqualVT tests equality comparison
579 +func TestEqualVT(t *testing.T) {
580 + t.Run("Packet", func(t *testing.T) {
581 + a := &Packet{Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST, Payload: []byte{0x01}}
582 + b := &Packet{Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST, Payload: []byte{0x01}}
583 + c := &Packet{Type: PacketType_PACKET_TYPE_LEASE_UPDATE_REQUEST, Payload: []byte{0x01}}
584 +
585 + if !a.EqualVT(b) {
586 + t.Error("Equal packets should be equal")
587 + }
588 + if a.EqualVT(c) {
589 + t.Error("Different packet types should not be equal")
590 + }
591 + if a.EqualVT(nil) {
592 + t.Error("Packet should not equal nil")
593 + }
594 + if !(*Packet)(nil).EqualVT(nil) {
595 + t.Error("nil should equal nil")
596 + }
597 + })
598 +
599 + t.Run("Lease", func(t *testing.T) {
600 + identity := &rdsec.Identity{Id: "test"}
601 + a := &Lease{Identity: identity, Expires: 123, Name: "test"}
602 + b := &Lease{Identity: identity, Expires: 123, Name: "test"}
603 + c := &Lease{Identity: identity, Expires: 456, Name: "test"}
604 +
605 + if !a.EqualVT(b) {
606 + t.Error("Equal leases should be equal")
607 + }
608 + if a.EqualVT(c) {
609 + t.Error("Leases with different Expires should not be equal")
610 + }
611 + })
612 +}
613 +
614 +// TestSizeVT tests size calculation accuracy
615 +func TestSizeVT(t *testing.T) {
616 + t.Run("Packet", func(t *testing.T) {
617 + msg := &Packet{
618 + Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST,
619 + Payload: []byte("test payload"),
620 + }
621 +
622 + size := msg.SizeVT()
623 + data, err := msg.MarshalVT()
624 + if err != nil {
625 + t.Fatalf("MarshalVT() error = %v", err)
626 + }
627 +
628 + if size != len(data) {
629 + t.Errorf("SizeVT() = %v, MarshalVT() produced %v bytes", size, len(data))
630 + }
631 + })
632 +
633 + t.Run("RelayInfo", func(t *testing.T) {
634 + msg := &RelayInfo{
635 + Identity: &rdsec.Identity{Id: "size-test"},
636 + Address: []string{"addr1:8080", "addr2:8080"},
637 + Leases: []*Lease{
638 + {Identity: &rdsec.Identity{Id: "l1"}, Name: "lease1"},
639 + },
640 + }
641 +
642 + size := msg.SizeVT()
643 + data, err := msg.MarshalVT()
644 + if err != nil {
645 + t.Fatalf("MarshalVT() error = %v", err)
646 + }
647 +
648 + if size != len(data) {
649 + t.Errorf("SizeVT() = %v, MarshalVT() produced %v bytes", size, len(data))
650 + }
651 + })
652 +
653 + t.Run("Lease", func(t *testing.T) {
654 + msg := &Lease{
655 + Identity: &rdsec.Identity{Id: "size-lease"},
656 + Expires: 1234567890,
657 + Name: "size-test",
658 + Alpn: []string{"h2", "http/1.1"},
659 + Metadata: "size metadata",
660 + }
661 +
662 + size := msg.SizeVT()
663 + data, err := msg.MarshalVT()
664 + if err != nil {
665 + t.Fatalf("MarshalVT() error = %v", err)
666 + }
667 +
668 + if size != len(data) {
669 + t.Errorf("SizeVT() = %v, MarshalVT() produced %v bytes", size, len(data))
670 + }
671 + })
672 +}
673 +
674 +// TestReset tests Reset clears all fields
675 +func TestReset(t *testing.T) {
676 + t.Run("Packet", func(t *testing.T) {
677 + p := &Packet{Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST, Payload: []byte{0x01}}
678 + p.Reset()
679 + if p.Type != 0 || p.Payload != nil {
680 + t.Error("Packet not properly reset")
681 + }
682 + })
683 +
684 + t.Run("Lease", func(t *testing.T) {
685 + l := &Lease{
686 + Identity: &rdsec.Identity{Id: "test"},
687 + Expires: 123,
688 + Name: "test-name",
689 + Alpn: []string{"h2"},
690 + Metadata: "meta",
691 + }
692 + l.Reset()
693 + if l.Identity != nil || l.Expires != 0 || l.Name != "" || l.Alpn != nil || l.Metadata != "" {
694 + t.Error("Lease not properly reset")
695 + }
696 + })
697 +}
698 +
699 +// TestGetters tests getter methods
700 +func TestGetters(t *testing.T) {
701 + t.Run("Packet", func(t *testing.T) {
702 + p := &Packet{
703 + Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST,
704 + Payload: []byte{0x01, 0x02},
705 + }
706 +
707 + if p.GetType() != PacketType_PACKET_TYPE_CONNECTION_REQUEST {
708 + t.Error("GetType() returned wrong value")
709 + }
710 + if !bytes.Equal(p.GetPayload(), []byte{0x01, 0x02}) {
711 + t.Error("GetPayload() returned wrong value")
712 + }
713 + })
714 +
715 + t.Run("Lease", func(t *testing.T) {
716 + identity := &rdsec.Identity{Id: "lease-id", PublicKey: []byte{0x01}}
717 + l := &Lease{
718 + Identity: identity,
719 + Expires: 12345,
720 + Name: "lease-name",
721 + Alpn: []string{"h2", "grpc"},
722 + Metadata: "lease-metadata",
723 + }
724 +
725 + if l.GetIdentity() != identity {
726 + t.Error("GetIdentity() returned wrong value")
727 + }
728 + if l.GetExpires() != 12345 {
729 + t.Error("GetExpires() returned wrong value")
730 + }
731 + if l.GetName() != "lease-name" {
732 + t.Error("GetName() returned wrong value")
733 + }
734 + if len(l.GetAlpn()) != 2 {
735 + t.Error("GetAlpn() returned wrong value")
736 + }
737 + if l.GetMetadata() != "lease-metadata" {
738 + t.Error("GetMetadata() returned wrong value")
739 + }
740 + })
741 +
742 + t.Run("nil Lease", func(t *testing.T) {
743 + var l *Lease
744 + if l.GetIdentity() != nil {
745 + t.Error("GetIdentity() on nil should return nil")
746 + }
747 + if l.GetExpires() != 0 {
748 + t.Error("GetExpires() on nil should return 0")
749 + }
750 + if l.GetName() != "" {
751 + t.Error("GetName() on nil should return empty string")
752 + }
753 + if l.GetAlpn() != nil {
754 + t.Error("GetAlpn() on nil should return nil")
755 + }
756 + if l.GetMetadata() != "" {
757 + t.Error("GetMetadata() on nil should return empty string")
758 + }
759 + })
760 +}
761 +
762 +// TestNilHandling tests nil message handling
763 +func TestNilHandling(t *testing.T) {
764 + testCases := []struct {
765 + name string
766 + test func(t *testing.T)
767 + }{
768 + {"Packet", func(t *testing.T) {
769 + var msg *Packet
770 + if data, err := msg.MarshalVT(); err != nil || data != nil {
771 + t.Errorf("MarshalVT() on nil Packet = (%v, %v), want (nil, nil)", data, err)
772 + }
773 + if msg.CloneVT() != nil {
774 + t.Error("CloneVT() on nil Packet should return nil")
775 + }
776 + if msg.SizeVT() != 0 {
777 + t.Error("SizeVT() on nil Packet should return 0")
778 + }
779 + }},
780 + {"RelayInfo", func(t *testing.T) {
781 + var msg *RelayInfo
782 + if msg.CloneVT() != nil {
783 + t.Error("CloneVT() on nil RelayInfo should return nil")
784 + }
785 + }},
786 + {"Lease", func(t *testing.T) {
787 + var msg *Lease
788 + if msg.CloneVT() != nil {
789 + t.Error("CloneVT() on nil Lease should return nil")
790 + }
791 + }},
792 + {"LeaseUpdateRequest", func(t *testing.T) {
793 + var msg *LeaseUpdateRequest
794 + if msg.CloneVT() != nil {
795 + t.Error("CloneVT() on nil LeaseUpdateRequest should return nil")
796 + }
797 + }},
798 + {"LeaseUpdateResponse", func(t *testing.T) {
799 + var msg *LeaseUpdateResponse
800 + if msg.CloneVT() != nil {
801 + t.Error("CloneVT() on nil LeaseUpdateResponse should return nil")
802 + }
803 + }},
804 + {"LeaseDeleteRequest", func(t *testing.T) {
805 + var msg *LeaseDeleteRequest
806 + if msg.CloneVT() != nil {
807 + t.Error("CloneVT() on nil LeaseDeleteRequest should return nil")
808 + }
809 + }},
810 + {"LeaseDeleteResponse", func(t *testing.T) {
811 + var msg *LeaseDeleteResponse
812 + if msg.CloneVT() != nil {
813 + t.Error("CloneVT() on nil LeaseDeleteResponse should return nil")
814 + }
815 + }},
816 + {"ConnectionRequest", func(t *testing.T) {
817 + var msg *ConnectionRequest
818 + if msg.CloneVT() != nil {
819 + t.Error("CloneVT() on nil ConnectionRequest should return nil")
820 + }
821 + }},
822 + {"ConnectionResponse", func(t *testing.T) {
823 + var msg *ConnectionResponse
824 + if msg.CloneVT() != nil {
825 + t.Error("CloneVT() on nil ConnectionResponse should return nil")
826 + }
827 + }},
828 + }
829 +
830 + for _, tc := range testCases {
831 + t.Run(tc.name, tc.test)
832 + }
833 +}
834 +
835 +// TestMarshalVTStrict tests strict marshaling
836 +func TestMarshalVTStrict(t *testing.T) {
837 + msg := &Packet{
838 + Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST,
839 + Payload: []byte{0x01, 0x02, 0x03},
840 + }
841 +
842 + data, err := msg.MarshalVTStrict()
843 + if err != nil {
844 + t.Fatalf("MarshalVTStrict() error = %v", err)
845 + }
846 +
847 + got := &Packet{}
848 + err = got.UnmarshalVT(data)
849 + if err != nil {
850 + t.Fatalf("UnmarshalVT() error = %v", err)
851 + }
852 +
853 + if !msg.EqualVT(got) {
854 + t.Error("MarshalVTStrict roundtrip mismatch")
855 + }
856 +}
857 +
858 +// TestUnmarshalVTUnsafe tests unsafe unmarshaling
859 +func TestUnmarshalVTUnsafe(t *testing.T) {
860 + msg := &ConnectionRequest{
861 + LeaseId: "unsafe-test",
862 + ClientIdentity: &rdsec.Identity{Id: "unsafe-client", PublicKey: []byte{0xAA, 0xBB}},
863 + }
864 +
865 + data, err := msg.MarshalVT()
866 + if err != nil {
867 + t.Fatalf("MarshalVT() error = %v", err)
868 + }
869 +
870 + got := &ConnectionRequest{}
871 + err = got.UnmarshalVTUnsafe(data)
872 + if err != nil {
873 + t.Fatalf("UnmarshalVTUnsafe() error = %v", err)
874 + }
875 +
876 + if !msg.EqualVT(got) {
877 + t.Error("UnmarshalVTUnsafe roundtrip mismatch")
878 + }
879 +}
880 +
881 +// TestEmptyResponseMessages tests response messages
882 +func TestEmptyResponseMessages(t *testing.T) {
883 + responses := []ResponseCode{
884 + ResponseCode_RESPONSE_CODE_UNKNOWN,
885 + ResponseCode_RESPONSE_CODE_ACCEPTED,
886 + ResponseCode_RESPONSE_CODE_INVALID_EXPIRES,
887 + ResponseCode_RESPONSE_CODE_INVALID_IDENTITY,
888 + ResponseCode_RESPONSE_CODE_INVALID_NAME,
889 + ResponseCode_RESPONSE_CODE_INVALID_ALPN,
890 + ResponseCode_RESPONSE_CODE_REJECTED,
891 + }
892 +
893 + for _, code := range responses {
894 + t.Run(code.String(), func(t *testing.T) {
895 + updateResp := &LeaseUpdateResponse{Code: code}
896 + data, err := updateResp.MarshalVT()
897 + if err != nil {
898 + t.Fatalf("MarshalVT() error = %v", err)
899 + }
900 +
901 + got := &LeaseUpdateResponse{}
902 + if err := got.UnmarshalVT(data); err != nil {
903 + t.Fatalf("UnmarshalVT() error = %v", err)
904 + }
905 +
906 + if got.Code != code {
907 + t.Errorf("Code = %v, want %v", got.Code, code)
908 + }
909 + })
910 + }
911 +}
912 +
913 +// TestComplexRelayInfo tests complex RelayInfo with multiple nested elements
914 +func TestComplexRelayInfo(t *testing.T) {
915 + // Create a complex RelayInfo with multiple addresses and leases
916 + msg := &RelayInfo{
917 + Identity: &rdsec.Identity{
918 + Id: "complex-relay",
919 + PublicKey: bytes.Repeat([]byte{0xAA}, 32),
920 + },
921 + Address: []string{
922 + "relay1.example.com:443",
923 + "relay2.example.com:443",
924 + "relay3.example.com:443",
925 + },
926 + Leases: []*Lease{
927 + {
928 + Identity: &rdsec.Identity{
929 + Id: "lease-1",
930 + PublicKey: bytes.Repeat([]byte{0x01}, 32),
931 + },
932 + Expires: 1000000000,
933 + Name: "service-1",
934 + Alpn: []string{"h2", "grpc"},
935 + Metadata: "production service 1",
936 + },
937 + {
938 + Identity: &rdsec.Identity{
939 + Id: "lease-2",
940 + PublicKey: bytes.Repeat([]byte{0x02}, 32),
941 + },
942 + Expires: 2000000000,
943 + Name: "service-2",
944 + Alpn: []string{"h2"},
945 + Metadata: "production service 2",
946 + },
947 + },
948 + }
949 +
950 + data, err := msg.MarshalVT()
951 + if err != nil {
952 + t.Fatalf("MarshalVT() error = %v", err)
953 + }
954 +
955 + got := &RelayInfo{}
956 + err = got.UnmarshalVT(data)
957 + if err != nil {
958 + t.Fatalf("UnmarshalVT() error = %v", err)
959 + }
960 +
961 + if !msg.EqualVT(got) {
962 + t.Error("complex RelayInfo roundtrip mismatch")
963 + }
964 +
965 + // Verify all fields
966 + if len(got.Address) != 3 {
967 + t.Errorf("got %d addresses, want 3", len(got.Address))
968 + }
969 + if len(got.Leases) != 2 {
970 + t.Errorf("got %d leases, want 2", len(got.Leases))
971 + }
972 +}
973 +
974 +// BenchmarkPacket_MarshalVT benchmarks packet marshaling
975 +func BenchmarkPacket_MarshalVT(b *testing.B) {
976 + msg := &Packet{
977 + Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST,
978 + Payload: bytes.Repeat([]byte{0x01}, 1024),
979 + }
980 +
981 + b.ResetTimer()
982 + for i := 0; i < b.N; i++ {
983 + _, _ = msg.MarshalVT()
984 + }
985 +}
986 +
987 +// BenchmarkPacket_UnmarshalVT benchmarks packet unmarshaling
988 +func BenchmarkPacket_UnmarshalVT(b *testing.B) {
989 + msg := &Packet{
990 + Type: PacketType_PACKET_TYPE_CONNECTION_REQUEST,
991 + Payload: bytes.Repeat([]byte{0x01}, 1024),
992 + }
993 +
994 + data, _ := msg.MarshalVT()
995 +
996 + b.ResetTimer()
997 + for i := 0; i < b.N; i++ {
998 + got := &Packet{}
999 + _ = got.UnmarshalVT(data)
1000 + }
1001 +}
1002 +
1003 +// BenchmarkRelayInfo_MarshalVT benchmarks complex relay info marshaling
1004 +func BenchmarkRelayInfo_MarshalVT(b *testing.B) {
1005 + msg := &RelayInfo{
1006 + Identity: &rdsec.Identity{
1007 + Id: "benchmark-relay",
1008 + PublicKey: bytes.Repeat([]byte{0xAA}, 32),
1009 + },
1010 + Address: []string{"addr1:8080", "addr2:8080", "addr3:8080"},
1011 + Leases: []*Lease{
1012 + {
1013 + Identity: &rdsec.Identity{Id: "l1", PublicKey: bytes.Repeat([]byte{0x01}, 32)},
1014 + Expires: 1234567890,
1015 + Name: "lease1",
1016 + Alpn: []string{"h2", "grpc"},
1017 + },
1018 + {
1019 + Identity: &rdsec.Identity{Id: "l2", PublicKey: bytes.Repeat([]byte{0x02}, 32)},
1020 + Expires: 9876543210,
1021 + Name: "lease2",
1022 + Alpn: []string{"h2"},
1023 + },
1024 + },
1025 + }
1026 +
1027 + b.ResetTimer()
1028 + for i := 0; i < b.N; i++ {
1029 + _, _ = msg.MarshalVT()
1030 + }
1031 +}
1032 +
1033 +// BenchmarkLease_MarshalVT benchmarks lease marshaling
1034 +func BenchmarkLease_MarshalVT(b *testing.B) {
1035 + msg := &Lease{
1036 + Identity: &rdsec.Identity{
1037 + Id: "benchmark-lease",
1038 + PublicKey: bytes.Repeat([]byte{0xBB}, 32),
1039 + },
1040 + Expires: 1234567890,
1041 + Name: "benchmark-lease",
1042 + Alpn: []string{"h2", "grpc", "http/1.1"},
1043 + Metadata: "benchmark metadata",
1044 + }
1045 +
1046 + b.ResetTimer()
1047 + for i := 0; i < b.N; i++ {
1048 + _, _ = msg.MarshalVT()
1049 + }
1050 +}
1051 +
1052 +// TestPacketType_String tests enum String method
1053 +func TestPacketType_String(t *testing.T) {
1054 + tests := []struct {
1055 + name string
1056 + enum PacketType
1057 + want string
1058 + }{
1059 + {"PACKET_TYPE_RELAY_INFO_REQUEST", PacketType_PACKET_TYPE_RELAY_INFO_REQUEST, "PACKET_TYPE_RELAY_INFO_REQUEST"},
1060 + {"PACKET_TYPE_RELAY_INFO_RESPONSE", PacketType_PACKET_TYPE_RELAY_INFO_RESPONSE, "PACKET_TYPE_RELAY_INFO_RESPONSE"},
1061 + {"PACKET_TYPE_LEASE_UPDATE_REQUEST", PacketType_PACKET_TYPE_LEASE_UPDATE_REQUEST, "PACKET_TYPE_LEASE_UPDATE_REQUEST"},
1062 + {"PACKET_TYPE_LEASE_UPDATE_RESPONSE", PacketType_PACKET_TYPE_LEASE_UPDATE_RESPONSE, "PACKET_TYPE_LEASE_UPDATE_RESPONSE"},
1063 + {"PACKET_TYPE_LEASE_DELETE_REQUEST", PacketType_PACKET_TYPE_LEASE_DELETE_REQUEST, "PACKET_TYPE_LEASE_DELETE_REQUEST"},
1064 + {"PACKET_TYPE_LEASE_DELETE_RESPONSE", PacketType_PACKET_TYPE_LEASE_DELETE_RESPONSE, "PACKET_TYPE_LEASE_DELETE_RESPONSE"},
1065 + {"PACKET_TYPE_CONNECTION_REQUEST", PacketType_PACKET_TYPE_CONNECTION_REQUEST, "PACKET_TYPE_CONNECTION_REQUEST"},
1066 + {"PACKET_TYPE_CONNECTION_RESPONSE", PacketType_PACKET_TYPE_CONNECTION_RESPONSE, "PACKET_TYPE_CONNECTION_RESPONSE"},
1067 + }
1068 +
1069 + for _, tt := range tests {
1070 + t.Run(tt.name, func(t *testing.T) {
1071 + if got := tt.enum.String(); got != tt.want {
1072 + t.Errorf("PacketType.String() = %v, want %v", got, tt.want)
1073 + }
1074 + })
1075 + }
1076 +}
1077 +
1078 +// TestPacketType_Enum tests Enum method
1079 +func TestPacketType_Enum(t *testing.T) {
1080 + if PacketType_PACKET_TYPE_CONNECTION_REQUEST.Enum() != nil && *PacketType_PACKET_TYPE_CONNECTION_REQUEST.Enum() != 6 {
1081 + t.Error("PacketType.Enum() returned wrong value")
1082 + }
1083 + if PacketType_PACKET_TYPE_RELAY_INFO_REQUEST.Enum() != nil && *PacketType_PACKET_TYPE_RELAY_INFO_REQUEST.Enum() != 0 {
1084 + t.Error("PACKET_TYPE_RELAY_INFO_REQUEST.Enum() should be 0")
1085 + }
1086 +}
1087 +
1088 +// TestResponseCode_String tests enum String method
1089 +func TestResponseCode_String(t *testing.T) {
1090 + tests := []struct {
1091 + name string
1092 + enum ResponseCode
1093 + want string
1094 + }{
1095 + {"RESPONSE_CODE_UNKNOWN", ResponseCode_RESPONSE_CODE_UNKNOWN, "RESPONSE_CODE_UNKNOWN"},
1096 + {"RESPONSE_CODE_ACCEPTED", ResponseCode_RESPONSE_CODE_ACCEPTED, "RESPONSE_CODE_ACCEPTED"},
1097 + {"RESPONSE_CODE_INVALID_EXPIRES", ResponseCode_RESPONSE_CODE_INVALID_EXPIRES, "RESPONSE_CODE_INVALID_EXPIRES"},
1098 + {"RESPONSE_CODE_INVALID_IDENTITY", ResponseCode_RESPONSE_CODE_INVALID_IDENTITY, "RESPONSE_CODE_INVALID_IDENTITY"},
1099 + {"RESPONSE_CODE_INVALID_NAME", ResponseCode_RESPONSE_CODE_INVALID_NAME, "RESPONSE_CODE_INVALID_NAME"},
1100 + {"RESPONSE_CODE_INVALID_ALPN", ResponseCode_RESPONSE_CODE_INVALID_ALPN, "RESPONSE_CODE_INVALID_ALPN"},
1101 + {"RESPONSE_CODE_REJECTED", ResponseCode_RESPONSE_CODE_REJECTED, "RESPONSE_CODE_REJECTED"},
1102 + }
1103 +
1104 + for _, tt := range tests {
1105 + t.Run(tt.name, func(t *testing.T) {
1106 + if got := tt.enum.String(); got != tt.want {
1107 + t.Errorf("ResponseCode.String() = %v, want %v", got, tt.want)
1108 + }
1109 + })
1110 + }
1111 +}
1112 +
1113 +// TestResponseCode_Enum tests Enum method
1114 +func TestResponseCode_Enum(t *testing.T) {
1115 + if ResponseCode_RESPONSE_CODE_ACCEPTED.Enum() != nil && *ResponseCode_RESPONSE_CODE_ACCEPTED.Enum() != 1 {
1116 + t.Error("ResponseCode.Enum() returned wrong value")
1117 + }
1118 + if ResponseCode_RESPONSE_CODE_UNKNOWN.Enum() != nil && *ResponseCode_RESPONSE_CODE_UNKNOWN.Enum() != 0 {
1119 + t.Error("RESPONSE_CODE_UNKNOWN.Enum() should be 0")
1120 + }
1121 +}
1122 +
1123 +// TestRelayInfo_Getters tests all getter methods
1124 +func TestRelayInfo_Getters(t *testing.T) {
1125 + identity := &rdsec.Identity{Id: "relay-test", PublicKey: []byte{0xAA}}
1126 + msg := &RelayInfo{
1127 + Identity: identity,
1128 + Address: []string{"addr1:8080", "addr2:8080"},
1129 + Leases: []*Lease{{Name: "lease1"}},
1130 + }
1131 +
1132 + if got := msg.GetIdentity(); got == nil || got.Id != "relay-test" {
1133 + t.Errorf("GetIdentity() = %v, want Id='relay-test'", got)
1134 + }
1135 + if got := msg.GetAddress(); len(got) != 2 {
1136 + t.Errorf("GetAddress() length = %v, want 2", len(got))
1137 + }
1138 + if got := msg.GetLeases(); len(got) != 1 {
1139 + t.Errorf("GetLeases() length = %v, want 1", len(got))
1140 + }
1141 +
1142 + // Test nil defaults
1143 + empty := &RelayInfo{}
1144 + if got := empty.GetIdentity(); got != nil {
1145 + t.Errorf("empty GetIdentity() = %v, want nil", got)
1146 + }
1147 + if got := empty.GetAddress(); got != nil {
1148 + t.Errorf("empty GetAddress() = %v, want nil", got)
1149 + }
1150 + if got := empty.GetLeases(); got != nil {
1151 + t.Errorf("empty GetLeases() = %v, want nil", got)
1152 + }
1153 +}
1154 +
1155 +// TestRelayInfo_Reset tests Reset method
1156 +func TestRelayInfo_Reset(t *testing.T) {
1157 + msg := &RelayInfo{
1158 + Identity: &rdsec.Identity{Id: "test"},
1159 + Address: []string{"addr1"},
1160 + Leases: []*Lease{{Name: "lease1"}},
1161 + }
1162 +
1163 + msg.Reset()
1164 +
1165 + if msg.Identity != nil {
1166 + t.Error("Reset() did not clear Identity")
1167 + }
1168 + if msg.Address != nil {
1169 + t.Error("Reset() did not clear Address")
1170 + }
1171 + if msg.Leases != nil {
1172 + t.Error("Reset() did not clear Leases")
1173 + }
1174 +}
1175 +
1176 +// TestRelayInfoRequest_Reset tests Reset method
1177 +func TestRelayInfoRequest_Reset(t *testing.T) {
1178 + msg := &RelayInfoRequest{}
1179 + msg.Reset() // Should not panic
1180 +}
1181 +
1182 +// TestRelayInfoResponse_Getters tests all getter methods
1183 +func TestRelayInfoResponse_Getters(t *testing.T) {
1184 + relay := &RelayInfo{Identity: &rdsec.Identity{Id: "response-test"}}
1185 + msg := &RelayInfoResponse{
1186 + RelayInfo: relay,
1187 + }
1188 +
1189 + if got := msg.GetRelayInfo(); got == nil || got.Identity.Id != "response-test" {
1190 + t.Errorf("GetRelayInfo() = %v, want Id='response-test'", got)
1191 + }
1192 +
1193 + // Test nil defaults
1194 + empty := &RelayInfoResponse{}
1195 + if got := empty.GetRelayInfo(); got != nil {
1196 + t.Errorf("empty GetRelayInfo() = %v, want nil", got)
1197 + }
1198 +}
1199 +
1200 +// TestRelayInfoResponse_Reset tests Reset method
1201 +func TestRelayInfoResponse_Reset(t *testing.T) {
1202 + msg := &RelayInfoResponse{
1203 + RelayInfo: &RelayInfo{Identity: &rdsec.Identity{Id: "test"}},
1204 + }
1205 +
1206 + msg.Reset()
1207 +
1208 + if msg.RelayInfo != nil {
1209 + t.Error("Reset() did not clear RelayInfo")
1210 + }
1211 +}
1212 +
1213 +// TestLeaseUpdateRequest_Getters tests all getter methods
1214 +func TestLeaseUpdateRequest_Getters(t *testing.T) {
1215 + lease := &Lease{Name: "update-lease"}
1216 + msg := &LeaseUpdateRequest{
1217 + Lease: lease,
1218 + Nonce: []byte{0x01, 0x02},
1219 + Timestamp: 1234567890,
1220 + }
1221 +
1222 + if got := msg.GetLease(); got == nil || got.Name != "update-lease" {
1223 + t.Errorf("GetLease() = %v, want Name='update-lease'", got)
1224 + }
1225 + if got := msg.GetNonce(); !bytes.Equal(got, []byte{0x01, 0x02}) {
1226 + t.Errorf("GetNonce() = %v, want [1 2]", got)
1227 + }
1228 + if got := msg.GetTimestamp(); got != 1234567890 {
1229 + t.Errorf("GetTimestamp() = %v, want 1234567890", got)
1230 + }
1231 +
1232 + // Test nil defaults
1233 + empty := &LeaseUpdateRequest{}
1234 + if got := empty.GetLease(); got != nil {
1235 + t.Errorf("empty GetLease() = %v, want nil", got)
1236 + }
1237 + if got := empty.GetNonce(); got != nil {
1238 + t.Errorf("empty GetNonce() = %v, want nil", got)
1239 + }
1240 +}
1241 +
1242 +// TestLeaseUpdateRequest_Reset tests Reset method
1243 +func TestLeaseUpdateRequest_Reset(t *testing.T) {
1244 + msg := &LeaseUpdateRequest{
1245 + Lease: &Lease{Name: "test"},
1246 + Nonce: []byte{0x01},
1247 + Timestamp: 123,
1248 + }
1249 +
1250 + msg.Reset()
1251 +
1252 + if msg.Lease != nil {
1253 + t.Error("Reset() did not clear Lease")
1254 + }
1255 + if msg.Nonce != nil {
1256 + t.Error("Reset() did not clear Nonce")
1257 + }
1258 + if msg.Timestamp != 0 {
1259 + t.Error("Reset() did not clear Timestamp")
1260 + }
1261 +}
1262 +
1263 +// TestLeaseUpdateResponse_Reset tests Reset method
1264 +func TestLeaseUpdateResponse_Reset(t *testing.T) {
1265 + msg := &LeaseUpdateResponse{
1266 + Code: ResponseCode_RESPONSE_CODE_ACCEPTED,
1267 + }
1268 +
1269 + msg.Reset()
1270 +
1271 + if msg.Code != ResponseCode_RESPONSE_CODE_UNKNOWN {
1272 + t.Error("Reset() did not clear Code to default")
1273 + }
1274 +}
1275 +
1276 +// TestLeaseDeleteRequest_Reset tests Reset method
1277 +func TestLeaseDeleteRequest_Reset(t *testing.T) {
1278 + msg := &LeaseDeleteRequest{
1279 + Identity: &rdsec.Identity{Id: "delete-test"},
1280 + }
1281 +
1282 + msg.Reset()
1283 +
1284 + if msg.Identity != nil {
1285 + t.Error("Reset() did not clear Identity")
1286 + }
1287 +}
1288 +
1289 +// TestLeaseDeleteResponse_Reset tests Reset method
1290 +func TestLeaseDeleteResponse_Reset(t *testing.T) {
1291 + msg := &LeaseDeleteResponse{
1292 + Code: ResponseCode_RESPONSE_CODE_ACCEPTED,
1293 + }
1294 +
1295 + msg.Reset()
1296 +
1297 + if msg.Code != ResponseCode_RESPONSE_CODE_UNKNOWN {
1298 + t.Error("Reset() did not clear Code to default")
1299 + }
1300 +}
portal/utils/wsstream/wsstream.go
+46 -16
@@ -8,35 +8,65 @@ import (
8 "github.com/gorilla/websocket"
9 )
10
11 +// webSocketConn defines the interface for WebSocket connections.
12 +// This allows for mocking in tests while using the real websocket.Conn in production.
13 +type webSocketConn interface {
14 + NextReader() (int, io.Reader, error)
15 + WriteMessage(int, []byte) error
16 + Close() error
17 +}
18 +
19 +// WsStream wraps a WebSocket connection to implement io.Reader and io.Writer.
20 type WsStream struct {
12 - Conn *websocket.Conn
21 + Conn webSocketConn
22 currentReader io.Reader
23 writeMu sync.Mutex
24 readMu sync.Mutex
25 }
26
27 +// New creates a new WsStream from a gorilla/websocket connection.
28 +func New(conn *websocket.Conn) *WsStream {
29 + return &WsStream{
30 + Conn: conn,
31 + }
32 +}
33 +
34 func (g *WsStream) Read(p []byte) (n int, err error) {
35 g.readMu.Lock()
36 defer g.readMu.Unlock()
21 - if g.currentReader == nil {
22 - _, reader, err := g.Conn.NextReader()
23 - if err != nil {
24 - return 0, err
25 - }
26 - g.currentReader = reader
27 - }
37
29 - n, err = g.currentReader.Read(p)
30 - if err == io.EOF {
31 - g.currentReader = nil
32 - err = nil
38 + // Handle empty buffer - standard io.Reader behavior
39 + if len(p) == 0 {
40 + return 0, nil
41 }
42
35 - if err != nil && strings.HasPrefix(err.Error(), "websocket: close ") {
36 - return 0, io.EOF
37 - }
43 + for {
44 + // Get a reader if we don't have one
45 + if g.currentReader == nil {
46 + _, reader, err := g.Conn.NextReader()
47 + if err != nil {
48 + // Convert websocket close errors to io.EOF
49 + if err != nil && strings.HasPrefix(err.Error(), "websocket: close ") {
50 + return 0, io.EOF
51 + }
52 + return 0, err
53 + }
54 + g.currentReader = reader
55 + }
56 +
57 + n, err = g.currentReader.Read(p)
58 + if err == io.EOF {
59 + // Current message exhausted, try to get next one
60 + g.currentReader = nil
61 + continue
62 + }
63
39 - return n, err
64 + if err != nil && strings.HasPrefix(err.Error(), "websocket: close ") {
65 + return 0, io.EOF
66 + }
67 +
68 + return n, err
69 + }
70 }
71
72 func (g *WsStream) Write(p []byte) (n int, err error) {
portal/utils/wsstream/wsstream_test.go new
+450
@@ -0,0 +1,450 @@
1 +package wsstream
2 +
3 +import (
4 + "bytes"
5 + "errors"
6 + "io"
7 + "sync"
8 + "testing"
9 +
10 + "github.com/gorilla/websocket"
11 +)
12 +
13 +// mockWebSocketConn is a mock implementation of websocket.Conn for testing
14 +type mockWebSocketConn struct {
15 + mu sync.Mutex
16 + readData [][]byte
17 + readIndex int
18 + writeData [][]byte
19 + closeCalled bool
20 + nextReaderErr error
21 + writeMessageErr error
22 + closeErr error
23 +}
24 +
25 +func newMockConn(data []byte) *mockWebSocketConn {
26 + return &mockWebSocketConn{
27 + readData: [][]byte{data},
28 + }
29 +}
30 +
31 +func (m *mockWebSocketConn) NextReader() (messageType int, r io.Reader, err error) {
32 + m.mu.Lock()
33 + defer m.mu.Unlock()
34 +
35 + if m.nextReaderErr != nil {
36 + return 0, nil, m.nextReaderErr
37 + }
38 +
39 + if m.readIndex >= len(m.readData) {
40 + return 0, nil, io.EOF
41 + }
42 +
43 + data := m.readData[m.readIndex]
44 + m.readIndex++
45 + return websocket.BinaryMessage, bytes.NewReader(data), nil
46 +}
47 +
48 +func (m *mockWebSocketConn) WriteMessage(messageType int, data []byte) error {
49 + m.mu.Lock()
50 + defer m.mu.Unlock()
51 +
52 + if m.writeMessageErr != nil {
53 + return m.writeMessageErr
54 + }
55 +
56 + // Copy data since caller may reuse the buffer
57 + dataCopy := make([]byte, len(data))
58 + copy(dataCopy, data)
59 + m.writeData = append(m.writeData, dataCopy)
60 + return nil
61 +}
62 +
63 +func (m *mockWebSocketConn) Close() error {
64 + m.mu.Lock()
65 + defer m.mu.Unlock()
66 + m.closeCalled = true
67 + return m.closeErr
68 +}
69 +
70 +// TestWsStream_Read tests the Read method
71 +func TestWsStream_Read(t *testing.T) {
72 + t.Run("single message", func(t *testing.T) {
73 + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
74 + mock := newMockConn(data)
75 + stream := &WsStream{Conn: mock}
76 +
77 + buf := make([]byte, 10)
78 + n, err := stream.Read(buf)
79 +
80 + if err != nil {
81 + t.Fatalf("Read() error = %v", err)
82 + }
83 + if n != 5 {
84 + t.Errorf("Read() n = %v, want 5", n)
85 + }
86 + if !bytes.Equal(buf[:5], data) {
87 + t.Errorf("Read() data = %v, want %v", buf[:5], data)
88 + }
89 + })
90 +
91 + t.Run("multiple reads from same message", func(t *testing.T) {
92 + data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
93 + mock := newMockConn(data)
94 + stream := &WsStream{Conn: mock}
95 +
96 + buf1 := make([]byte, 3)
97 + n1, err := stream.Read(buf1)
98 + if err != nil {
99 + t.Fatalf("First Read() error = %v", err)
100 + }
101 + if n1 != 3 {
102 + t.Errorf("First Read() n = %v, want 3", n1)
103 + }
104 +
105 + buf2 := make([]byte, 10)
106 + n2, err := stream.Read(buf2)
107 + if err != nil {
108 + t.Fatalf("Second Read() error = %v", err)
109 + }
110 + if n2 != 5 {
111 + t.Errorf("Second Read() n = %v, want 5", n2)
112 + }
113 + if !bytes.Equal(buf2[:5], []byte{0x04, 0x05, 0x06, 0x07, 0x08}) {
114 + t.Errorf("Second Read() data = %v, want [4 5 6 7 8]", buf2[:5])
115 + }
116 + })
117 +
118 + t.Run("multiple messages", func(t *testing.T) {
119 + mock := &mockWebSocketConn{
120 + readData: [][]byte{
121 + {0x01, 0x02},
122 + {0x03, 0x04},
123 + },
124 + }
125 + stream := &WsStream{Conn: mock}
126 +
127 + buf := make([]byte, 10)
128 + n1, err := stream.Read(buf)
129 + if err != nil {
130 + t.Fatalf("First Read() error = %v", err)
131 + }
132 + if n1 != 2 {
133 + t.Errorf("First Read() n = %v, want 2", n1)
134 + }
135 +
136 + n2, err := stream.Read(buf)
137 + if err != nil {
138 + t.Fatalf("Second Read() error = %v", err)
139 + }
140 + if n2 != 2 {
141 + t.Errorf("Second Read() n = %v, want 2", n2)
142 + }
143 + })
144 +
145 + t.Run("empty buffer", func(t *testing.T) {
146 + mock := newMockConn([]byte{0x01})
147 + stream := &WsStream{Conn: mock}
148 +
149 + buf := make([]byte, 0)
150 + n, err := stream.Read(buf)
151 + if err != nil {
152 + t.Fatalf("Read() error = %v", err)
153 + }
154 + if n != 0 {
155 + t.Errorf("Read() n = %v, want 0", n)
156 + }
157 + })
158 +
159 + t.Run("EOF after message", func(t *testing.T) {
160 + mock := newMockConn([]byte{0x01, 0x02})
161 + stream := &WsStream{Conn: mock}
162 +
163 + buf := make([]byte, 10)
164 + _, err := stream.Read(buf)
165 + if err != nil {
166 + t.Fatalf("First Read() error = %v", err)
167 + }
168 +
169 + _, err = stream.Read(buf)
170 + if err != io.EOF {
171 + t.Errorf("Second Read() error = %v, want io.EOF", err)
172 + }
173 + })
174 +
175 + t.Run("NextReader error", func(t *testing.T) {
176 + mock := &mockWebSocketConn{
177 + nextReaderErr: errors.New("connection reset"),
178 + }
179 + stream := &WsStream{Conn: mock}
180 +
181 + buf := make([]byte, 10)
182 + _, err := stream.Read(buf)
183 + if err == nil {
184 + t.Error("Read() error = nil, want error")
185 + }
186 + })
187 +
188 + t.Run("websocket close error", func(t *testing.T) {
189 + mock := &mockWebSocketConn{
190 + nextReaderErr: &websocket.CloseError{
191 + Code: websocket.CloseNormalClosure,
192 + Text: "normal closure",
193 + },
194 + }
195 + stream := &WsStream{Conn: mock}
196 +
197 + buf := make([]byte, 10)
198 + _, err := stream.Read(buf)
199 + if err != io.EOF {
200 + t.Errorf("Read() error = %v, want io.EOF", err)
201 + }
202 + })
203 +
204 + t.Run("concurrent reads", func(t *testing.T) {
205 + data := []byte{0x01, 0x02, 0x03, 0x04}
206 + mock := &mockWebSocketConn{
207 + readData: [][]byte{data, data},
208 + }
209 + stream := &WsStream{Conn: mock}
210 +
211 + var wg sync.WaitGroup
212 + errors := make(chan error, 2)
213 +
214 + for i := 0; i < 2; i++ {
215 + wg.Add(1)
216 + go func() {
217 + defer wg.Done()
218 + buf := make([]byte, 4)
219 + _, err := stream.Read(buf)
220 + errors <- err
221 + }()
222 + }
223 +
224 + wg.Wait()
225 + close(errors)
226 +
227 + for err := range errors {
228 + if err != nil && err != io.EOF {
229 + t.Errorf("Concurrent Read() error = %v", err)
230 + }
231 + }
232 + })
233 +}
234 +
235 +// TestWsStream_Write tests the Write method
236 +func TestWsStream_Write(t *testing.T) {
237 + t.Run("successful write", func(t *testing.T) {
238 + mock := newMockConn(nil)
239 + stream := &WsStream{Conn: mock}
240 +
241 + data := []byte{0x01, 0x02, 0x03}
242 + n, err := stream.Write(data)
243 +
244 + if err != nil {
245 + t.Fatalf("Write() error = %v", err)
246 + }
247 + if n != 3 {
248 + t.Errorf("Write() n = %v, want 3", n)
249 + }
250 + if len(mock.writeData) != 1 {
251 + t.Errorf("Write() messages written = %v, want 1", len(mock.writeData))
252 + }
253 + if !bytes.Equal(mock.writeData[0], data) {
254 + t.Errorf("Write() data = %v, want %v", mock.writeData[0], data)
255 + }
256 + })
257 +
258 + t.Run("empty write", func(t *testing.T) {
259 + mock := newMockConn(nil)
260 + stream := &WsStream{Conn: mock}
261 +
262 + data := []byte{}
263 + n, err := stream.Write(data)
264 +
265 + if err != nil {
266 + t.Fatalf("Write() error = %v", err)
267 + }
268 + if n != 0 {
269 + t.Errorf("Write() n = %v, want 0", n)
270 + }
271 + })
272 +
273 + t.Run("multiple writes", func(t *testing.T) {
274 + mock := newMockConn(nil)
275 + stream := &WsStream{Conn: mock}
276 +
277 + stream.Write([]byte{0x01})
278 + stream.Write([]byte{0x02})
279 + stream.Write([]byte{0x03})
280 +
281 + if len(mock.writeData) != 3 {
282 + t.Errorf("Write() messages written = %v, want 3", len(mock.writeData))
283 + }
284 + })
285 +
286 + t.Run("write error", func(t *testing.T) {
287 + mock := &mockWebSocketConn{
288 + writeMessageErr: errors.New("write failed"),
289 + }
290 + stream := &WsStream{Conn: mock}
291 +
292 + data := []byte{0x01, 0x02}
293 + _, err := stream.Write(data)
294 +
295 + if err == nil {
296 + t.Error("Write() error = nil, want error")
297 + }
298 + })
299 +
300 + t.Run("websocket close error returns EOF", func(t *testing.T) {
301 + mock := &mockWebSocketConn{
302 + writeMessageErr: &websocket.CloseError{
303 + Code: websocket.CloseGoingAway,
304 + Text: "going away",
305 + },
306 + }
307 + stream := &WsStream{Conn: mock}
308 +
309 + data := []byte{0x01, 0x02}
310 + _, err := stream.Write(data)
311 +
312 + if err != io.EOF {
313 + t.Errorf("Write() error = %v, want io.EOF", err)
314 + }
315 + })
316 +
317 + t.Run("concurrent writes", func(t *testing.T) {
318 + mock := newMockConn(nil)
319 + stream := &WsStream{Conn: mock}
320 +
321 + var wg sync.WaitGroup
322 + for i := 0; i < 10; i++ {
323 + wg.Add(1)
324 + go func(b byte) {
325 + defer wg.Done()
326 + stream.Write([]byte{b})
327 + }(byte(i))
328 + }
329 +
330 + wg.Wait()
331 +
332 + if len(mock.writeData) != 10 {
333 + t.Errorf("Concurrent Write() messages = %v, want 10", len(mock.writeData))
334 + }
335 + })
336 +}
337 +
338 +// TestWsStream_Close tests the Close method
339 +func TestWsStream_Close(t *testing.T) {
340 + t.Run("successful close", func(t *testing.T) {
341 + mock := newMockConn(nil)
342 + stream := &WsStream{Conn: mock}
343 +
344 + err := stream.Close()
345 +
346 + if err != nil {
347 + t.Fatalf("Close() error = %v", err)
348 + }
349 + if !mock.closeCalled {
350 + t.Error("Close() did not call underlying Conn.Close()")
351 + }
352 + })
353 +
354 + t.Run("close with error", func(t *testing.T) {
355 + mock := &mockWebSocketConn{
356 + closeErr: errors.New("close failed"),
357 + }
358 + stream := &WsStream{Conn: mock}
359 +
360 + err := stream.Close()
361 +
362 + if err == nil {
363 + t.Error("Close() error = nil, want error")
364 + }
365 + })
366 +
367 + t.Run("multiple closes", func(t *testing.T) {
368 + mock := newMockConn(nil)
369 + stream := &WsStream{Conn: mock}
370 +
371 + stream.Close()
372 + err := stream.Close()
373 +
374 + // Second close should not panic, just return whatever Conn.Close returns
375 + if !mock.closeCalled {
376 + t.Error("Close() did not call underlying Conn.Close()")
377 + }
378 + _ = err // We don't care about the error on second close
379 + })
380 +}
381 +
382 +// TestWsStream_ReadWrite tests full read-write cycle
383 +func TestWsStream_ReadWrite(t *testing.T) {
384 + t.Run("full cycle", func(t *testing.T) {
385 + mock := newMockConn(nil)
386 + stream := &WsStream{Conn: mock}
387 +
388 + // Write some data
389 + writeData := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
390 + _, err := stream.Write(writeData)
391 + if err != nil {
392 + t.Fatalf("Write() error = %v", err)
393 + }
394 +
395 + // Verify write
396 + if len(mock.writeData) != 1 {
397 + t.Fatalf("Write() messages = %v, want 1", len(mock.writeData))
398 + }
399 +
400 + // Now test reading
401 + readMock := &mockWebSocketConn{
402 + readData: [][]byte{writeData},
403 + }
404 + readStream := &WsStream{Conn: readMock}
405 +
406 + buf := make([]byte, 10)
407 + n, err := readStream.Read(buf)
408 + if err != nil {
409 + t.Fatalf("Read() error = %v", err)
410 + }
411 +
412 + if n != 5 {
413 + t.Errorf("Read() n = %v, want 5", n)
414 + }
415 + if !bytes.Equal(buf[:5], writeData) {
416 + t.Errorf("Read() data = %v, want %v", buf[:5], writeData)
417 + }
418 + })
419 +}
420 +
421 +// BenchmarkWsStream_Read benchmarks the Read method
422 +func BenchmarkWsStream_Read(b *testing.B) {
423 + data := make([]byte, 1024)
424 + mock := newMockConn(data)
425 + stream := &WsStream{Conn: mock}
426 +
427 + buf := make([]byte, 1024)
428 +
429 + b.ResetTimer()
430 + for i := 0; i < b.N; i++ {
431 + stream.Read(buf)
432 + // Reset for next iteration
433 + if i%1000 == 999 {
434 + mock.readIndex = 0
435 + }
436 + }
437 +}
438 +
439 +// BenchmarkWsStream_Write benchmarks the Write method
440 +func BenchmarkWsStream_Write(b *testing.B) {
441 + mock := newMockConn(nil)
442 + stream := &WsStream{Conn: mock}
443 +
444 + data := make([]byte, 1024)
445 +
446 + b.ResetTimer()
447 + for i := 0; i < b.N; i++ {
448 + stream.Write(data)
449 + }
450 +}
utils/ws.go
+2 -2
@@ -22,7 +22,7 @@ func NewWebSocketDialer() func(context.Context, string) (io.ReadWriteCloser, err
22 return nil, err
23 }
24 // Response body is closed by the Dialer on successful connection
25 - return &wsstream.WsStream{Conn: wsConn}, nil
25 + return wsstream.New(wsConn), nil
26 }
27 }
28
@@ -42,5 +42,5 @@ func UpgradeToWSStream(w http.ResponseWriter, r *http.Request, responseHeader ht
42 if err != nil {
43 return nil, nil, err
44 }
45 - return &wsstream.WsStream{Conn: wsConn}, wsConn, nil
45 + return wsstream.New(wsConn), wsConn, nil
46 }