Skip to content
File

Blob: tun/client/connection_test.go

go127 lines
1package client
2 
3import (
4 "context"
5 "errors"
6 "os"
7 "testing"
8 
9 "go.miragespace.co/specter/spec/chord"
10 "go.miragespace.co/specter/spec/mocks"
11 "go.miragespace.co/specter/spec/protocol"
12 "go.miragespace.co/specter/spec/rpc"
13 
14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"
16 "github.com/zhangyunhao116/skipmap"
17 "go.uber.org/zap/zaptest"
18)
19 
20func TestPublishPreferenceRTT(t *testing.T) {
21 as := require.New(t)
22 logger := zaptest.NewLogger(t)
23 
24 file, err := os.CreateTemp("", "client")
25 as.NoError(err)
26 defer os.Remove(file.Name())
27 
28 ctx := t.Context()
29 
30 token := &protocol.ClientToken{
31 Token: []byte("test"),
32 }
33 cl := &protocol.Node{
34 Id: chord.Random(),
35 }
36 
37 der, cert, key := makeCertificate(as, logger, cl, token, nil)
38 cfg := &Config{
39 path: file.Name(),
40 router: skipmap.NewString[route](),
41 Apex: testApex,
42 Certificate: cert,
43 PrivKey: key,
44 Tunnels: []Tunnel{
45 {
46 Target: "tcp://127.0.0.1:2345",
47 },
48 },
49 }
50 as.NoError(cfg.validate())
51 
52 m := func(s *mocks.TunnelService, t1 *mocks.MemoryTransport, publishCall *mock.Call) {
53 defaultNoHostnames(s)
54 transportHelper(t1, der)
55 }
56 
57 client, _, assertion := setupClient(t, as, ctx, logger, nil, cfg, nil, m, true, 1)
58 defer assertion()
59 defer client.Close()
60 
61 client.Start(ctx)
62}
63 
64func TestReconciliationRecoversFromCompleteOutage(t *testing.T) {
65 c, tunnelClient := newOutcomeTestClient(t, []Tunnel{{
66 Hostname: "configured",
67 Target: "http://localhost:8080",
68 }})
69 original := c.getConnectedNodes()
70 tunnelClient.TunnelService.On("RegisteredHostnames", mock.Anything, mock.Anything).
71 Return(&protocol.RegisteredHostnamesResponse{}, nil).Once()
72 tunnelClient.TunnelService.On("PublishTunnel", mock.Anything, mock.Anything).
73 Return(&protocol.PublishTunnelResponse{Published: original}, nil).Once()
74 require.Empty(t, c.SyncConfigTunnels(t.Context()).Error)
75 saved, err := os.ReadFile(c.Configuration.path)
76 require.NoError(t, err)
77 
78 gateway := mock.MatchedBy(func(ctx context.Context) bool {
79 return rpc.GetNode(ctx).GetAddress() == original[0].GetAddress()
80 })
81 apex := mock.MatchedBy(func(ctx context.Context) bool {
82 return rpc.GetNode(ctx).GetAddress() == c.Configuration.Apex
83 })
84 tunnelClient.TunnelService.On("Ping", gateway, mock.Anything).
85 Return(nil, errors.New("gateway disconnected")).Once()
86 tunnelClient.TunnelService.On("Ping", apex, mock.Anything).
87 Return(nil, errors.New("apex temporarily unavailable")).Once()
88 
89 // Losing every gateway clears prior publication acknowledgements even when
90 // the first bootstrap fails. Recovery remains pending for a later pass.
91 c.reconcileConnections(t.Context())
92 require.Empty(t, c.getConnectedNodes())
93 require.True(t, c.getStatus().Pending)
94 require.False(t, c.getStatus().Synchronization.Tunnels[0].Published)
95 tunnelClient.TunnelService.AssertNumberOfCalls(t, "PublishTunnel", 1)
96 
97 peer := &protocol.Node{
98 Id: 2,
99 Address: "peer.example.com",
100 }
101 recovered := []*protocol.Node{original[0], peer}
102 tunnelClient.TunnelService.On("Ping", apex, mock.Anything).
103 Return(&protocol.ClientPingResponse{Node: original[0]}, nil).Once()
104 tunnelClient.TunnelService.On("Ping", gateway, mock.Anything).
105 Return(&protocol.ClientPingResponse{Node: original[0]}, nil).Once()
106 tunnelClient.TunnelService.On("Ping", mock.MatchedBy(func(ctx context.Context) bool {
107 return rpc.GetNode(ctx).GetAddress() == peer.GetAddress()
108 }), mock.Anything).Return(&protocol.ClientPingResponse{Node: peer}, nil).Twice()
109 tunnelClient.TunnelService.On("GetNodes", gateway, mock.Anything).
110 Return(&protocol.GetNodesResponse{Nodes: recovered}, nil).Once()
111 tunnelClient.TunnelService.On("PublishTunnel", mock.Anything, mock.MatchedBy(func(req *protocol.PublishTunnelRequest) bool {
112 return req.GetHostname() == "configured" && endpointSignature(req.GetServers()) == endpointSignature(recovered)
113 })).Return(&protocol.PublishTunnelResponse{Published: recovered}, nil).Once()
114 
115 // One pass bootstraps, discovers another peer, and republishes without a
116 // separate worker or a delay between those steps.
117 c.reconcileConnections(t.Context())
118 require.Equal(t, endpointSignature(recovered), endpointSignature(c.getConnectedNodes()))
119 status := c.getStatus()
120 require.False(t, status.Pending)
121 require.Empty(t, status.Synchronization.Error)
122 require.Equal(t, 2, status.Synchronization.Tunnels[0].PublishedEndpoints)
123 onDisk, err := os.ReadFile(c.Configuration.path)
124 require.NoError(t, err)
125 require.Equal(t, saved, onDisk, "reconnection must not rewrite the configuration")
126}