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 "sort"
6 "strings"
7 "testing"
8
9 "github.com/aws/aws-sdk-go-v2/aws"
10 "github.com/aws/aws-sdk-go-v2/service/route53"
11 r53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
12)
13
14// fakeRoute53 satisfies route53API and records the record-set changes.
15type fakeRoute53 struct {
16 zones map[string]string // zone name (with trailing dot) → id
17 changes []string // "ACTION|name|value"
18}
19
20// route53SortKey mirrors the real ListHostedZonesByName ordering: zone names
21// are compared with their dot-separated labels reversed (so "tito.io."
22// becomes "io.tito"), which groups zones under shared parents together.
23func route53SortKey(name string) string {
24 labels := strings.Split(strings.ToLower(strings.TrimSuffix(name, ".")), ".")
25 rev := make([]string, len(labels))
26 for i, l := range labels {
27 rev[len(labels)-1-i] = l
28 }
29 return strings.Join(rev, ".")
30}
31
32// ListHostedZonesByName models the real API's contract: DNSName is not a
33// filter, it is the start point of a listing sorted by route53SortKey. Only
34// zones whose sort key is >= the start key are returned — a real, and
35// initially unnoticed, source of the Critical bug this fake now exists to
36// catch: a parent zone of the query fqdn sorts BEFORE that fqdn's own key
37// and is silently absent from a listing started at the fqdn itself.
38func (f *fakeRoute53) ListHostedZonesByName(_ context.Context, in *route53.ListHostedZonesByNameInput, _ ...func(*route53.Options)) (*route53.ListHostedZonesByNameOutput, error) {
39 start := route53SortKey(aws.ToString(in.DNSName))
40 type entry struct{ name, id, key string }
41 var entries []entry
42 for name, id := range f.zones {
43 entries = append(entries, entry{name: name, id: id, key: route53SortKey(name)})
44 }
45 sort.Slice(entries, func(i, j int) bool { return entries[i].key < entries[j].key })
46 out := &route53.ListHostedZonesByNameOutput{}
47 for _, e := range entries {
48 if e.key >= start {
49 out.HostedZones = append(out.HostedZones, r53types.HostedZone{Name: aws.String(e.name), Id: aws.String("/hostedzone/" + e.id)})
50 }
51 }
52 return out, nil
53}
54
55func (f *fakeRoute53) ChangeResourceRecordSets(_ context.Context, in *route53.ChangeResourceRecordSetsInput, _ ...func(*route53.Options)) (*route53.ChangeResourceRecordSetsOutput, error) {
56 for _, c := range in.ChangeBatch.Changes {
57 f.changes = append(f.changes, string(c.Action)+"|"+aws.ToString(c.ResourceRecordSet.Name)+"|"+aws.ToString(c.ResourceRecordSet.ResourceRecords[0].Value))
58 }
59 return &route53.ChangeResourceRecordSetsOutput{ChangeInfo: &r53types.ChangeInfo{Id: aws.String("c1"), Status: r53types.ChangeStatusInsync}}, nil
60}
61
62func (f *fakeRoute53) GetChange(_ context.Context, _ *route53.GetChangeInput, _ ...func(*route53.Options)) (*route53.GetChangeOutput, error) {
63 return &route53.GetChangeOutput{ChangeInfo: &r53types.ChangeInfo{Status: r53types.ChangeStatusInsync}}, nil
64}
65
66// The fqdn seam: the backend must find the zone that is a parent of the
67// record fqdn — tito.io owns _acme-challenge.go.tito.io when go.tito.io has
68// no zone of its own, and the value must be quoted (Route53 stores TXT
69// values as quoted character-strings). "example.com." is an unrelated
70// second zone in the account, present to prove the fake's DNSName
71// start-point filtering (and the real code's exact-match walk) don't get
72// tripped up by an unrelated zone sharing the account.
73//
74// This fixture is also the regression case for the Critical bug: tito.io.'s
75// route53SortKey ("io.tito") sorts BEFORE _acme-challenge.go.tito.io.'s own
76// key ("io.tito.go._acme-challenge"), so the old implementation — which
77// called ListHostedZonesByName with the full fqdn as DNSName and then
78// suffix-matched the results — never saw tito.io. in the listing and always
79// returned "no hosted zone" here.
80func TestRoute53PresentUpsertsAndCleansUp(t *testing.T) {
81 fake := &fakeRoute53{zones: map[string]string{"tito.io.": "Z1", "example.com.": "Z9"}}
82 r := &route53DNS{client: fake, zoneIDs: map[string]string{}}
83
84 cleanup, err := r.present(context.Background(), "_acme-challenge.go.tito.io", "txtvalue")
85 if err != nil {
86 t.Fatalf("present: %v", err)
87 }
88 if len(fake.changes) != 1 || fake.changes[0] != `UPSERT|_acme-challenge.go.tito.io.|"txtvalue"` {
89 t.Fatalf("changes after present = %v", fake.changes)
90 }
91 if err := cleanup(context.Background()); err != nil {
92 t.Fatalf("cleanup: %v", err)
93 }
94 if len(fake.changes) != 2 || !strings.HasPrefix(fake.changes[1], "DELETE|_acme-challenge.go.tito.io.|") {
95 t.Fatalf("changes after cleanup = %v", fake.changes)
96 }
97 if got := r.zoneIDs["_acme-challenge.go.tito.io"]; got != "Z1" {
98 t.Fatalf("cached zone id = %q, want Z1 (parent zone tito.io.)", got)
99 }
100}
101
102// TestRoute53PrefersMostSpecificZone covers the case where the challenge
103// fqdn's immediate parent (go.tito.io.) has its own hosted zone alongside
104// the grandparent (tito.io.) — the more specific zone must win, because the
105// candidate walk tries go.tito.io. before tito.io. and returns on first hit.
106func TestRoute53PrefersMostSpecificZone(t *testing.T) {
107 fake := &fakeRoute53{zones: map[string]string{"tito.io.": "Z1", "go.tito.io.": "Z2"}}
108 r := &route53DNS{client: fake, zoneIDs: map[string]string{}}
109
110 if _, err := r.present(context.Background(), "_acme-challenge.go.tito.io", "txtvalue"); err != nil {
111 t.Fatalf("present: %v", err)
112 }
113 if got := r.zoneIDs["_acme-challenge.go.tito.io"]; got != "Z2" {
114 t.Fatalf("cached zone id = %q, want Z2 (most specific parent zone go.tito.io.)", got)
115 }
116}
117
118func TestRoute53NoZone(t *testing.T) {
119 fake := &fakeRoute53{zones: map[string]string{"example.org.": "Z9"}}
120 r := &route53DNS{client: fake, zoneIDs: map[string]string{}}
121 if _, err := r.present(context.Background(), "_acme-challenge.go.tito.io", "v"); err == nil {
122 t.Fatal("present with no owning zone: want error, got nil")
123 }
124}
125