carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "os"
8 "path/filepath"
9 "reflect"
10 "sort"
11 "strings"
12 "sync"
13 "sync/atomic"
14 "testing"
15 "time"
16
17 "golang.org/x/crypto/acme/autocert"
18
19 "github.com/carlosframework/platform/internal/domains"
20 "github.com/carlosframework/platform/internal/registry"
21)
22
23// waitWoke polls an atomic wake counter — the wake is fired off the
24// refresh goroutine, so a test observes it, never assumes it.
25func waitWoke(t *testing.T, woke *atomic.Int32, want int32) {
26 t.Helper()
27 deadline := time.Now().Add(2 * time.Second)
28 for woke.Load() != want {
29 if time.Now().After(deadline) {
30 t.Fatalf("woke = %d; want %d", woke.Load(), want)
31 }
32 time.Sleep(time.Millisecond)
33 }
34}
35
36// #300: the home map exposes the deployment's platform-class names, and a
37// changed platform name set wakes whoever asked (the wildcard manager).
38func TestHomeMapHostsAndWakeOnChange(t *testing.T) {
39 s := meshTestStore(t)
40 putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"own.acct.apps.test":{"role":"home","platform":true}}}`)
41 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"cribado.bad.apps.test":{"role":"home","platform":true},"*.catch.test":{"role":"home","platform":true},"stray.foo.apps.test":{"role":"home"}}}`)
42 m := newHomeMap(s, "annie")
43 var woke atomic.Int32
44 m.setHostsWake(func() { woke.Add(1) })
45 refreshHomeMap(t, m)
46 if got, want := m.Hosts(), []string{"cribado.bad.apps.test"}; !reflect.DeepEqual(got, want) {
47 t.Fatalf("Hosts = %v; want %v (own box, catch-all and unclassed rows excluded)", got, want)
48 }
49 waitWoke(t, &woke, 1)
50 refreshHomeMap(t, m) // unchanged: no wake
51 time.Sleep(20 * time.Millisecond)
52 if woke.Load() != 1 {
53 t.Fatalf("an unchanged refresh woke the manager (%d)", woke.Load())
54 }
55 // Home moved, and an UNCLASSED name came and went: still no wake.
56 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"3.3.3.3","hosts":{"cribado.bad.apps.test":{"role":"home","platform":true},"*.catch.test":{"role":"home","platform":true},"other.foo.apps.test":{"role":"home"}}}`)
57 refreshHomeMap(t, m)
58 time.Sleep(20 * time.Millisecond)
59 if woke.Load() != 1 {
60 t.Fatalf("a moved home / unclassed churn woke the manager (%d)", woke.Load())
61 }
62 // Same count, different platform name (a swap in one poll): wakes.
63 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"3.3.3.3","hosts":{"other.bad.apps.test":{"role":"home","platform":true},"*.catch.test":{"role":"home","platform":true}}}`)
64 refreshHomeMap(t, m)
65 waitWoke(t, &woke, 2)
66 putJSON(t, s, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"4.4.4.4","hosts":{"new.bae.apps.test":{"role":"home","platform":true}}}`)
67 refreshHomeMap(t, m)
68 waitWoke(t, &woke, 3)
69 if got, want := m.Hosts(), []string{"new.bae.apps.test", "other.bad.apps.test"}; !reflect.DeepEqual(got, want) {
70 t.Fatalf("Hosts = %v; want %v", got, want)
71 }
72
73 // A name LOSING its mark (a home box rolled back below the mark) is a
74 // change: the parent must leave, and the manager must hear about it.
75 putJSON(t, s, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"4.4.4.4","hosts":{"new.bae.apps.test":{"role":"home"}}}`)
76 refreshHomeMap(t, m)
77 waitWoke(t, &woke, 4)
78 if got, want := m.Hosts(), []string{"other.bad.apps.test"}; !reflect.DeepEqual(got, want) {
79 t.Fatalf("Hosts after unmark = %v; want %v", got, want)
80 }
81 // A marked catch-all coming and going moves nothing and wakes nobody.
82 putJSON(t, s, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"4.4.4.4","hosts":{"new.bae.apps.test":{"role":"home"},"*.claimed.apps.test":{"role":"home","platform":true}}}`)
83 refreshHomeMap(t, m)
84 time.Sleep(20 * time.Millisecond)
85 if woke.Load() != 4 {
86 t.Fatalf("a catch-all woke the manager (%d)", woke.Load())
87 }
88 if got, want := m.Hosts(), []string{"other.bad.apps.test"}; !reflect.DeepEqual(got, want) {
89 t.Fatalf("Hosts after catch-all = %v; want %v", got, want)
90 }
91
92 var nilMap *homeMap
93 if nilMap.Hosts() != nil {
94 t.Fatal("nil map must report no hosts")
95 }
96 nilMap.setHostsWake(func() {}) // no panic
97 // An unlabelled box (hand-run `carlos edge`) would fold its OWN file in
98 // as another box's: it reports nothing, as Home does.
99 unlabelled := newHomeMap(s, "")
100 refreshHomeMap(t, unlabelled)
101 if got := unlabelled.Hosts(); got != nil {
102 t.Fatalf("unlabelled box reported %v", got)
103 }
104}
105
106// A file written before the class mark existed carries no flag: nothing
107// in it derives a parent until its box republishes. Fail closed.
108func TestHomeMapHostsIgnoresPreMarkFiles(t *testing.T) {
109 s := meshTestStore(t)
110 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"cribado.bad.apps.test":{"role":"home"}}}`)
111 m := newHomeMap(s, "annie")
112 refreshHomeMap(t, m)
113 if got := m.Hosts(); len(got) != 0 {
114 t.Fatalf("pre-mark file contributed %v", got)
115 }
116 if _, _, ok := m.Home("cribado.bad.apps.test"); !ok {
117 t.Fatal("the class mark must not affect backhaul")
118 }
119}
120
121// platformHomedOn is homedOn with the publisher's class mark set — the
122// shape a real edges file has for platform-minted rows (#300).
123func platformHomedOn(t *testing.T, m *homeMap, box, eip string, hosts ...string) {
124 t.Helper()
125 entries := make([]string, 0, len(hosts))
126 for _, h := range hosts {
127 entries = append(entries, fmt.Sprintf("%q:{\"role\":\"home\",\"platform\":true}", h))
128 }
129 putJSON(t, m.s, "control/edges/"+box+".json", fmt.Sprintf(
130 `{"v":1,"box":%q,"eip":%q,"hosts":{%s}}`, box, eip, strings.Join(entries, ",")))
131 refreshHomeMap(t, m)
132}
133
134// deploymentHosts is this box's routes plus the home map's names, deduped.
135func TestDeploymentHostsUnionsTheHomeMap(t *testing.T) {
136 e, _, _ := remoteHomedEdge(t, []string{"own.acct.apps.test"})
137 platformHomedOn(t, e.homes.Load(), "betty", "2.2.2.2", "cribado.bad.apps.test", "own.acct.apps.test")
138 got := e.deploymentHosts()
139 sort.Strings(got)
140 if want := []string{"cribado.bad.apps.test", "own.acct.apps.test"}; !reflect.DeepEqual(got, want) {
141 t.Fatalf("deploymentHosts = %v; want %v", got, want)
142 }
143 // No deployment store, no home map: a single-box deployment is its own.
144 lone := testIssuerEdge("own.acct.apps.test")
145 if lone.homes.Load() != nil {
146 t.Fatal("test edge unexpectedly has a home map")
147 }
148 if got := lone.deploymentHosts(); !reflect.DeepEqual(got, lone.currentHosts()) {
149 t.Fatalf("no home map: deploymentHosts %v != currentHosts %v", got, lone.currentHosts())
150 }
151}
152
153// The manager built from env derives the parent of an app it only ever
154// backhauls to — the region-edge shape that failed on tito stage 6.
155func TestWildcardParentsIncludeRemoteHomedAccounts(t *testing.T) {
156 e, _, _ := remoteHomedEdge(t, []string{"static.bdj.apps.test"})
157 platformHomedOn(t, e.homes.Load(), "betty", "2.2.2.2", "cribado.bad.apps.test", "foreign.report")
158 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
159 if err != nil {
160 t.Fatal(err)
161 }
162 t.Cleanup(func() { reg.Close() })
163 e.reg = reg
164 t.Setenv("CARLOS_WILDCARD_DOMAINS", "")
165 t.Setenv("PLATFORM_APPS_DOMAIN", "apps.test")
166 t.Setenv("CARLOS_DNS_PROVIDER", "route53")
167 t.Setenv("CARLOS_ACME_DELEGATION_ZONE", "")
168 w := newWildcardFromEnv(e, autocert.DirCache(t.TempDir()))
169 if w == nil {
170 t.Fatal("want a manager")
171 }
172 w.refresh()
173 // testIssuerEdge's own rows carry no trust flag, so currentHosts (#290,
174 // pinned in wildcard_test.go) contributes nothing here: what is left is
175 // exactly the remote account — and never the foreign domain.
176 if got, want := w.currentParents(), []string{"bad.apps.test"}; !reflect.DeepEqual(got, want) {
177 t.Fatalf("parents = %v; want %v (remote account included, foreign domain not)", got, want)
178 }
179}
180
181// The production wiring, end to end: a remote box's file landing grows the
182// manager's parent set on the home map's poll, and its leaving shrinks it.
183func TestHomeMapWakeReachesTheWildcardManager(t *testing.T) {
184 e, _, base := remoteHomedEdge(t, nil)
185 platformHomedOn(t, e.homes.Load(), "betty", "2.2.2.2", "seed.bdj.apps.test")
186 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
187 if err != nil {
188 t.Fatal(err)
189 }
190 t.Cleanup(func() { reg.Close() })
191 e.reg = reg
192 t.Setenv("CARLOS_WILDCARD_DOMAINS", "")
193 t.Setenv("PLATFORM_APPS_DOMAIN", "apps.test")
194 t.Setenv("CARLOS_DNS_PROVIDER", "route53")
195 t.Setenv("CARLOS_ACME_DELEGATION_ZONE", "")
196 w := newWildcardFromEnv(e, autocert.DirCache(t.TempDir()))
197 hm := e.homes.Load()
198 hm.setHostsWake(w.refresh)
199 w.refresh()
200 if got := w.currentParents(); !reflect.DeepEqual(got, []string{"bdj.apps.test"}) {
201 t.Fatalf("seed parents = %v", got)
202 }
203 putJSON(t, base, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"4.4.4.4","hosts":{"cribado.bad.apps.test":{"role":"home","platform":true}}}`)
204 refreshHomeMap(t, hm)
205 waitParents(t, w, []string{"bad.apps.test", "bdj.apps.test"})
206 if err := base.Delete(context.Background(), "control/edges/carol.json"); err != nil {
207 t.Fatal(err)
208 }
209 refreshHomeMap(t, hm)
210 waitParents(t, w, []string{"bdj.apps.test"})
211}
212
213func waitParents(t *testing.T, w *wildcardManager, want []string) {
214 t.Helper()
215 deadline := time.Now().Add(2 * time.Second)
216 for {
217 if got := w.currentParents(); reflect.DeepEqual(got, want) {
218 return
219 }
220 if time.Now().After(deadline) {
221 t.Fatalf("parents = %v; want %v", w.currentParents(), want)
222 }
223 time.Sleep(time.Millisecond)
224 }
225}
226
227// refresh() is serialized: whatever interleaving the three callers produce,
228// the parent set that lands last is the one derived from the latest hosts.
229func TestWildcardRefreshIsSerialized(t *testing.T) {
230 var gen atomic.Int32
231 hosts := func() []string {
232 n := gen.Load()
233 // A slow read: the interleaving window the mutex closes.
234 time.Sleep(200 * time.Microsecond)
235 // Sqid-shaped labels: parentsFor derives a parent only from a label
236 // that could be an account (#215).
237 return []string{"app." + domains.Sqid(int64(n)) + ".apps.test"}
238 }
239 w := newWildcardManager(nil, "apps.test", hosts, func() []string { return nil }, "", autocert.DirCache(t.TempDir()), nil)
240 var wg sync.WaitGroup
241 for i := 0; i < 8; i++ {
242 wg.Add(1)
243 go func() {
244 defer wg.Done()
245 for j := 0; j < 25; j++ {
246 gen.Add(1)
247 w.refresh()
248 }
249 }()
250 }
251 wg.Wait()
252 final := gen.Load()
253 w.refresh()
254 if got := w.currentParents(); !reflect.DeepEqual(got, []string{domains.Sqid(int64(final)) + ".apps.test"}) {
255 t.Fatalf("parents = %v after generation %d", got, final)
256 }
257}
258
259// #287, reader side: an attached custom domain published under its own
260// name (never platform-class) backhauls, passes the cert gates' exact
261// admission, and derives no wildcard parent; its catch-all form dials but
262// is never an exact admission (#180).
263func TestAliasEntriesBackhaulAndAdmitButDeriveNoParent(t *testing.T) {
264 e, _, base := remoteHomedEdge(t, nil)
265 putJSON(t, base, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"www.customer.com":{"role":"home","platform":false,"alias":true},"*.customer.com":{"role":"home","platform":false,"alias":true},"app.acct.apps.test":{"role":"home","platform":true}}}`)
266 hm := e.homes.Load()
267 refreshHomeMap(t, hm)
268 if _, box, ok := hm.Home("www.customer.com"); !ok || box != "betty" {
269 t.Fatal("alias entry does not backhaul")
270 }
271 if _, _, ok := hm.Home("shop.customer.com"); !ok {
272 t.Fatal("catch-all alias does not dial one label under it")
273 }
274 if !e.deploymentHost("www.customer.com") {
275 t.Fatal("alias entry not admitted by the cert gates")
276 }
277 if e.deploymentHost("*.customer.com") || e.deploymentHost("shop.customer.com") {
278 t.Fatal("a catch-all alias must never be an exact cert admission (#180)")
279 }
280 if got := hm.Hosts(); !reflect.DeepEqual(got, []string{"app.acct.apps.test"}) {
281 t.Fatalf("customer names leaked into the parent input: %v", got)
282 }
283}
284
285// A route beats an alias on a cross-box name collision whatever the
286// boxes' sort order (#287): a tenant attaching an operator's hostname
287// captures nothing beyond its own box. And an alias can never smuggle the
288// platform mark.
289func TestHomeMapRouteBeatsAliasOnCollision(t *testing.T) {
290 for _, order := range [][2]string{{"aaa", "zzz"}, {"zzz", "aaa"}} {
291 routeBox, aliasBox := order[0], order[1]
292 s := meshTestStore(t)
293 putJSON(t, s, "control/edges/"+routeBox+".json", `{"v":1,"box":"`+routeBox+`","eip":"1.1.1.1","hosts":{"status.platform.test":{"role":"home","platform":false}}}`)
294 putJSON(t, s, "control/edges/"+aliasBox+".json", `{"v":1,"box":"`+aliasBox+`","eip":"2.2.2.2","hosts":{"status.platform.test":{"role":"home","platform":true,"alias":true}}}`)
295 m := newHomeMap(s, "self")
296 refreshHomeMap(t, m)
297 if _, box, ok := m.Home("status.platform.test"); !ok || box != routeBox {
298 t.Fatalf("order %v: home = %s; want the route's box %s", order, box, routeBox)
299 }
300 if got := m.Hosts(); len(got) != 0 {
301 t.Fatalf("order %v: an alias entry reached the parent input: %v", order, got)
302 }
303 }
304}
305
306// #305, reader side: a marked "*.<parent>" entry is a delegated parent for
307// the wildcard manager AND a dial target for hosts one label under it; an
308// unmarked catch-all (a pre-#305 file, or a route's own catch-all row) is
309// only the latter. A wildcard entry appearing wakes the manager.
310func TestHomeMapWildcardParents(t *testing.T) {
311 s := meshTestStore(t)
312 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true},"*.plain.example":{"role":"home"},"app.acct.apps.test":{"role":"home","platform":true}}}`)
313 m := newHomeMap(s, "annie")
314 var woke atomic.Int32
315 m.setHostsWake(func() { woke.Add(1) })
316 refreshHomeMap(t, m)
317 waitWoke(t, &woke, 1)
318 if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"customer.example"}) {
319 t.Fatalf("WildcardParents = %v", got)
320 }
321 if got := m.Hosts(); !reflect.DeepEqual(got, []string{"app.acct.apps.test"}) {
322 t.Fatalf("a wildcard entry leaked into Hosts: %v", got)
323 }
324 for _, h := range []string{"pds.customer.example", "x.plain.example"} {
325 if _, box, ok := m.Home(h); !ok || box != "betty" {
326 t.Errorf("%s: not dialed via the catch-all", h)
327 }
328 }
329 // A second delegated parent lands: wake.
330 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true},"*.second.example":{"role":"home","wildcard":true},"app.acct.apps.test":{"role":"home","platform":true}}}`)
331 refreshHomeMap(t, m)
332 waitWoke(t, &woke, 2)
333 if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"customer.example", "second.example"}) {
334 t.Fatalf("WildcardParents = %v", got)
335 }
336 // An unmarked catch-all coming or going still wakes nobody.
337 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true},"*.second.example":{"role":"home","wildcard":true},"app.acct.apps.test":{"role":"home","platform":true},"*.plain2.example":{"role":"home"}}}`)
338 refreshHomeMap(t, m)
339 time.Sleep(20 * time.Millisecond)
340 if woke.Load() != 2 {
341 t.Fatalf("an unmarked catch-all woke the manager (%d)", woke.Load())
342 }
343 var unlabelled *homeMap
344 if unlabelled.WildcardParents() != nil {
345 t.Fatal("nil map reported parents")
346 }
347}
348
349// The manager built from env on a region edge unions the remote delegated
350// parent: it is in the parent set AND delegated (challenge via the
351// delegation zone), alongside its own local rows.
352func TestWildcardManagerUnionsRemoteDelegatedParents(t *testing.T) {
353 e, _, base := remoteHomedEdge(t, nil)
354 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
355 if err != nil {
356 t.Fatal(err)
357 }
358 t.Cleanup(func() { reg.Close() })
359 e.reg = reg
360 if err := reg.UpsertWildcardDomain(registry.WildcardDomain{Host: "local.example", Account: "acct", App: "app"}); err != nil {
361 t.Fatal(err)
362 }
363 putJSON(t, base, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"home","wildcard":true}}}`)
364 refreshHomeMap(t, e.homes.Load())
365 t.Setenv("CARLOS_WILDCARD_DOMAINS", "")
366 t.Setenv("PLATFORM_APPS_DOMAIN", "apps.test")
367 t.Setenv("CARLOS_DNS_PROVIDER", "route53")
368 t.Setenv("CARLOS_ACME_DELEGATION_ZONE", "acme.apps.test")
369 w := newWildcardFromEnv(e, autocert.DirCache(t.TempDir()))
370 if w == nil {
371 t.Fatal("want a manager")
372 }
373 w.refresh()
374 if got := w.currentParents(); !reflect.DeepEqual(got, []string{"customer.example", "local.example"}) {
375 t.Fatalf("parents = %v", got)
376 }
377 if !w.isDelegated("customer.example") || !w.isDelegated("local.example") {
378 t.Fatal("remote or local delegated parent not marked delegated")
379 }
380}
381
382// Two boxes publishing the same "*.<parent>" key merge CLASSES and pick one
383// dial target (route beats alias, any beats none), whatever the sort order
384// — a delegated parent's PEM must load everywhere even when another box
385// routes the key too. A cert-only entry is a parent but never a dial target.
386func TestHomeMapWildcardCollisionMergesClasses(t *testing.T) {
387 for _, order := range [][2]string{{"aaa", "zzz"}, {"zzz", "aaa"}} {
388 routeBox, wildBox := order[0], order[1]
389 s := meshTestStore(t)
390 putJSON(t, s, "control/edges/"+routeBox+".json", `{"v":1,"box":"`+routeBox+`","eip":"1.1.1.1","hosts":{"*.customer.example":{"role":"home","platform":true}}}`)
391 putJSON(t, s, "control/edges/"+wildBox+".json", `{"v":1,"box":"`+wildBox+`","eip":"2.2.2.2","hosts":{"*.customer.example":{"role":"wildcard","wildcard":true},"*.certonly.example":{"role":"wildcard","wildcard":true}}}`)
392 m := newHomeMap(s, "self")
393 refreshHomeMap(t, m)
394 if _, box, ok := m.Home("x.customer.example"); !ok || box != routeBox {
395 t.Fatalf("order %v: dial = %s/%v; want the route's box %s", order, box, ok, routeBox)
396 }
397 if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"certonly.example", "customer.example"}) {
398 t.Fatalf("order %v: WildcardParents = %v", order, got)
399 }
400 if _, _, ok := m.Home("x.certonly.example"); ok {
401 t.Fatalf("order %v: a cert-only wildcard entry was a dial target", order)
402 }
403 }
404 // Role wildcard on an EXACT key is refused (no KnownHost admission
405 // without a dial); a file with no EIP still contributes cert halves.
406 s2 := meshTestStore(t)
407 putJSON(t, s2, "control/edges/carol.json", `{"v":1,"box":"carol","eip":"","hosts":{"*.noeip.example":{"role":"home","wildcard":true},"victim.example":{"role":"wildcard"},"served.example":{"role":"home","platform":true}}}`)
408 m2 := newHomeMap(s2, "self")
409 refreshHomeMap(t, m2)
410 if got := m2.WildcardParents(); !reflect.DeepEqual(got, []string{"noeip.example"}) {
411 t.Fatalf("EIP-less file: WildcardParents = %v", got)
412 }
413 if m2.KnownHost("victim.example") || m2.KnownHost("served.example") {
414 t.Fatal("an exact key from an EIP-less file or with role wildcard was admitted")
415 }
416 if _, _, ok := m2.Home("x.noeip.example"); ok {
417 t.Fatal("an EIP-less file offered a dial target")
418 }
419 // A remote parent of bad shape is not a parent.
420 s := meshTestStore(t)
421 putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"*.nodot":{"role":"wildcard","wildcard":true},"*.*.double.example":{"role":"wildcard","wildcard":true},"*.ok.example":{"role":"wildcard","wildcard":true}}}`)
422 m := newHomeMap(s, "self")
423 refreshHomeMap(t, m)
424 if got := m.WildcardParents(); !reflect.DeepEqual(got, []string{"ok.example"}) {
425 t.Fatalf("WildcardParents = %v", got)
426 }
427}
428
429// The fleet's delegated set is capped where the per-box table is: local
430// rows first, the overflow logged and dropped.
431func TestUnionDelegatedCapsAndWarns(t *testing.T) {
432 delegatedCapLogged.Store(0) // once-per-process throttles: isolate from -count>1
433 delegationZoneUnsetOnce = sync.Once{}
434 var buf strings.Builder
435 log.SetOutput(&buf)
436 t.Cleanup(func() { log.SetOutput(os.Stderr) })
437 local := []string{"l1.example", "l2.example"}
438 remote := make([]string, 0, 40)
439 for i := 0; i < 40; i++ {
440 remote = append(remote, fmt.Sprintf("r%02d.example", i))
441 }
442 remote = append(remote, "L1.example", "r00.example") // duplicates never cost a slot
443 got := unionDelegated(local, remote, "acme.apps.test")
444 if len(got) != maxDelegatedParents || got[0] != "l1.example" || got[1] != "l2.example" || got[2] != "r00.example" {
445 t.Fatalf("union = %d entries, head %v", len(got), got[:3])
446 }
447 if got := unionDelegated([]string{"a.example"}, []string{"A.example", "a.example", "b.example"}, "z"); !reflect.DeepEqual(got, []string{"a.example", "b.example"}) {
448 t.Fatalf("dedupe = %v", got)
449 }
450 if !strings.Contains(buf.String(), "capped at 32") {
451 t.Fatalf("overflow not logged:\n%s", buf.String())
452 }
453 buf.Reset()
454 if got := unionDelegated(local, []string{"r.example"}, ""); len(got) != 3 || !strings.Contains(buf.String(), "CARLOS_ACME_DELEGATION_ZONE is unset") {
455 t.Fatalf("no-zone case: %v / %s", got, buf.String())
456 }
457 buf.Reset()
458 unionDelegated(local, []string{"r.example"}, "") // a standing condition is said once
459 if buf.Len() != 0 {
460 t.Fatalf("zone-unset line repeated:\n%s", buf.String())
461 }
462 if got := unionDelegated(local, nil, ""); len(got) != 2 || buf.Len() != 0 {
463 t.Fatalf("local-only case logged or changed: %v / %s", got, buf.String())
464 }
465}
466