main
go 286 lines 8.16 KB
Raw
1 package sdk
2
3 import (
4 "context"
5 "errors"
6 "net/url"
7 "testing"
8
9 "github.com/gosuda/portal-tunnel/v2/portal/discovery"
10 "github.com/gosuda/portal-tunnel/v2/types"
11 "github.com/gosuda/portal-tunnel/v2/utils"
12 )
13
14 func mustRelaySet(t *testing.T, relayURLs ...string) *discovery.RelaySet {
15 t.Helper()
16 return discovery.NewRelaySet(relayURLs)
17 }
18
19 func TestExposureConfigSnapshotsDoNotShareMutableState(t *testing.T) {
20 exposure := &Exposure{
21 cfg: utils.NewSnapshot(ExposeConfig{
22 RelayURLs: []string{"https://relay-a.example"},
23 Identity: types.Identity{
24 Name: "svc",
25 Address: "portal-address",
26 },
27 Metadata: types.LeaseMetadata{
28 Tags: []string{"initial"},
29 },
30 }, ExposeConfig.snapshot),
31 }
32
33 snapshot := exposure.Config()
34 snapshot.RelayURLs[0] = "https://mutated.example"
35 snapshot.Metadata.Tags[0] = "mutated"
36
37 next := exposure.Config()
38 if got := next.RelayURLs[0]; got != "https://relay-a.example" {
39 t.Fatalf("RelayURLs[0] = %q, want original relay", got)
40 }
41 if got := next.Metadata.Tags[0]; got != "initial" {
42 t.Fatalf("Metadata.Tags[0] = %q, want original tag", got)
43 }
44
45 exposure.cfg.UpdateCopy(func(cfg *ExposeConfig) {
46 cfg.MaxActiveRelays = 2
47 cfg.Metadata = types.LeaseMetadata{Tags: []string{"updated"}}
48 })
49
50 metadata := exposure.Config().Metadata
51 metadata.Tags[0] = "mutated"
52 if got := exposure.Config().Metadata.Tags[0]; got != "updated" {
53 t.Fatalf("Metadata.Tags[0] = %q, want updated", got)
54 }
55 if got := exposure.Config().MaxActiveRelays; got != 2 {
56 t.Fatalf("MaxActiveRelays = %d, want 2", got)
57 }
58 }
59
60 func TestExposureReconcileRemovesBannedRelayFromActiveSet(t *testing.T) {
61 const (
62 relayA = "https://relay-a.example"
63 relayB = "https://relay-b.example"
64 )
65
66 relayURL, err := url.Parse(relayA)
67 if err != nil {
68 t.Fatalf("url.Parse() error = %v", err)
69 }
70 relayBURL, err := url.Parse(relayB)
71 if err != nil {
72 t.Fatalf("url.Parse() error = %v", err)
73 }
74
75 exposure := &Exposure{
76 cfg: utils.NewSnapshot(ExposeConfig{RelayURLs: []string{relayA, relayB}}, ExposeConfig.snapshot),
77 relaySet: mustRelaySet(t, relayA, relayB),
78 relayListeners: make(map[string]*listener, 2),
79 }
80 relayAClosed := make(chan struct{})
81 exposure.relayListeners = map[string]*listener{
82 relayA: {
83 relayURL: relayURL,
84 route: discovery.NewRoute([]string{relayA}, true),
85 cancel: func() { close(relayAClosed) },
86 doneCh: relayAClosed,
87 },
88 relayB: {
89 relayURL: relayBURL,
90 route: discovery.NewRoute([]string{relayB}, true),
91 },
92 }
93
94 exposure.relaySet.BanRelayURL(relayA)
95 if err := exposure.reconcileRelayListeners(false); err != nil {
96 t.Fatalf("reconcileRelayListeners() error = %v", err)
97 }
98
99 select {
100 case <-relayAClosed:
101 default:
102 t.Fatal("banned relay listener was not closed")
103 }
104
105 if got := exposure.ActiveRelayURLs(); len(got) != 1 || got[0] != relayB {
106 t.Fatalf("ActiveRelayURLs() = %v, want [%q]", got, relayB)
107 }
108
109 exposure.mu.RLock()
110 _, listenerExists := exposure.relayListeners[relayA]
111 exposure.mu.RUnlock()
112 if listenerExists {
113 t.Fatal("banned relay listener still exists in exposure.listeners")
114 }
115 }
116
117 func TestExposureReconcileRemovesStaleListener(t *testing.T) {
118 const (
119 relayA = "https://relay-a.example"
120 relayB = "https://relay-b.example"
121 )
122
123 relayAURL, err := url.Parse(relayA)
124 if err != nil {
125 t.Fatalf("url.Parse(relayA) error = %v", err)
126 }
127 relayBURL, err := url.Parse(relayB)
128 if err != nil {
129 t.Fatalf("url.Parse(relayB) error = %v", err)
130 }
131
132 relayAClosed := make(chan struct{})
133 exposure := &Exposure{
134 cfg: utils.NewSnapshot(ExposeConfig{RelayURLs: []string{relayB}}, ExposeConfig.snapshot),
135 relaySet: mustRelaySet(t, relayA, relayB),
136 relayListeners: make(map[string]*listener, 2),
137 }
138 exposure.relayListeners = map[string]*listener{
139 relayA: {
140 relayURL: relayAURL,
141 route: discovery.NewRoute([]string{relayA}, true),
142 cancel: func() { close(relayAClosed) },
143 doneCh: relayAClosed,
144 },
145 relayB: {
146 relayURL: relayBURL,
147 route: discovery.NewRoute([]string{relayB}, true),
148 },
149 }
150
151 exposure.relaySet.SetBootstrapRelayURLs([]string{relayB})
152 if err := exposure.reconcileRelayListeners(false); err != nil {
153 t.Fatalf("reconcileRelayListeners() error = %v", err)
154 }
155
156 select {
157 case <-relayAClosed:
158 default:
159 t.Fatal("stale relay listener was not closed")
160 }
161
162 knownRelayURLs := exposure.ActiveRelayURLs()
163 exposure.mu.RLock()
164 _, relayAExists := exposure.relayListeners[relayA]
165 _, relayBExists := exposure.relayListeners[relayB]
166 exposure.mu.RUnlock()
167 if len(knownRelayURLs) != 1 || knownRelayURLs[0] != relayB {
168 t.Fatalf("knownRelayURLs = %v, want [%q]", knownRelayURLs, relayB)
169 }
170 if relayAExists {
171 t.Fatal("stale relay listener still exists in exposure.listeners")
172 }
173 if !relayBExists {
174 t.Fatal("active relay listener missing from exposure.listeners")
175 }
176 }
177
178 func TestExposureRemoveRelayStopsRunningListener(t *testing.T) {
179 const relayA = "https://relay-a.example"
180
181 relayAURL, err := url.Parse(relayA)
182 if err != nil {
183 t.Fatalf("url.Parse(relayA) error = %v", err)
184 }
185
186 relayAClosed := make(chan struct{})
187 exposure := &Exposure{
188 cfg: utils.NewSnapshot(ExposeConfig{RelayURLs: []string{relayA}}, ExposeConfig.snapshot),
189 relaySet: mustRelaySet(t, relayA),
190 relayListeners: make(map[string]*listener, 1),
191 }
192 exposure.relayListeners[relayA] = &listener{
193 relayURL: relayAURL,
194 cancel: func() { close(relayAClosed) },
195 doneCh: relayAClosed,
196 }
197
198 if err := exposure.RemoveRelay(relayA); err != nil {
199 t.Fatalf("RemoveRelay() error = %v", err)
200 }
201
202 select {
203 case <-relayAClosed:
204 default:
205 t.Fatal("removed relay listener was not closed")
206 }
207 if got := exposure.ActiveRelayURLs(); len(got) != 0 {
208 t.Fatalf("ActiveRelayURLs() = %v, want empty", got)
209 }
210 if got := exposure.Config().RelayURLs; len(got) != 0 {
211 t.Fatalf("RelayURLs = %v, want empty", got)
212 }
213 if got := exposure.relaySet.PriorityRelays(discovery.RouteState{}); len(got) != 0 {
214 t.Fatalf("PriorityRelays() = %v, want empty", got)
215 }
216 relays := exposure.relaySet.AllRelays()
217 if len(relays) != 1 || relays[0].Descriptor.APIHTTPSAddr != relayA || relays[0].Banned {
218 t.Fatalf("AllRelays() = %+v, want unbanned candidate %q", relays, relayA)
219 }
220 }
221
222 func TestExposureListenerSelfExitKeepsExplicitRelayConfigured(t *testing.T) {
223 const relayA = "https://relay-a.example"
224
225 relayAURL, err := url.Parse(relayA)
226 if err != nil {
227 t.Fatalf("url.Parse(relayA) error = %v", err)
228 }
229
230 l := &listener{relayURL: relayAURL}
231 exposure := &Exposure{
232 cfg: utils.NewSnapshot(ExposeConfig{RelayURLs: []string{relayA}}, ExposeConfig.snapshot),
233 relaySet: mustRelaySet(t, relayA),
234 relayListeners: map[string]*listener{relayA: l},
235 done: make(chan struct{}),
236 }
237
238 exposure.runListenerAcceptLoop(l)
239
240 if got := exposure.ActiveRelayURLs(); len(got) != 0 {
241 t.Fatalf("ActiveRelayURLs() = %v, want empty", got)
242 }
243 if got := exposure.Config().RelayURLs; len(got) != 1 || got[0] != relayA {
244 t.Fatalf("RelayURLs = %v, want [%q]", got, relayA)
245 }
246 if got := exposure.relaySet.BootstrapRelayURLs(); len(got) != 1 || got[0] != relayA {
247 t.Fatalf("BootstrapRelayURLs() = %v, want [%q]", got, relayA)
248 }
249 }
250
251 func TestListenerRetryBudgetDropsAutoSelectedRelayWithoutPoolBan(t *testing.T) {
252 const relayA = "https://relay-a.example"
253
254 relayAURL, err := url.Parse(relayA)
255 if err != nil {
256 t.Fatalf("url.Parse(relayA) error = %v", err)
257 }
258
259 relaySet := mustRelaySet(t, relayA)
260 listener := &listener{
261 relayURL: relayAURL,
262 route: discovery.NewRoute([]string{relayA}, false),
263 relaySet: relaySet,
264 retryCount: 1,
265 }
266
267 if listener.waitRetry(context.Background(), "lease registration", errors.New("boom"), 2, 0) {
268 t.Fatal("waitRetry() = true after retry budget was exhausted")
269 }
270
271 routes, err := relaySet.PlanRoutes(nil, discovery.RouteState{})
272 if err != nil {
273 t.Fatalf("PlanRoutes() error = %v", err)
274 }
275 if len(routes) != 0 {
276 t.Fatalf("PlanRoutes() = %v, want no active routes", routes)
277 }
278
279 relays := relaySet.AllRelays()
280 if len(relays) != 1 || relays[0].Banned || relays[0].Descriptor.APIHTTPSAddr != relayA {
281 t.Fatalf("AllRelays() = %+v, want relay retained outside active pool", relays)
282 }
283 if got := relaySet.BootstrapRelayURLs(); len(got) != 1 || got[0] != relayA {
284 t.Fatalf("BootstrapRelayURLs() = %v, want [%q]", got, relayA)
285 }
286 }