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 "encoding/json"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "reflect"
11 "testing"
12)
13
14func TestSplitDomains(t *testing.T) {
15 cases := []struct {
16 in string
17 want []string
18 }{
19 {"", nil},
20 {"go.tito.io", []string{"go.tito.io"}},
21 {" Go.Tito.IO , dev.oncarlos.com ,", []string{"go.tito.io", "dev.oncarlos.com"}},
22 }
23 for _, c := range cases {
24 if got := splitDomains(c.in); !reflect.DeepEqual(got, c.want) {
25 t.Errorf("splitDomains(%q) = %v, want %v", c.in, got, c.want)
26 }
27 }
28}
29
30func TestDNSProviderSelection(t *testing.T) {
31 t.Setenv("CARLOS_DNS_PROVIDER", "")
32 t.Setenv("CARLOS_DNSIMPLE_TOKEN", "")
33 if p, reason := dnsProviderFromEnv(); p != nil || reason == "" {
34 t.Fatalf("unset env: want (nil, reason), got (%v, %q)", p, reason)
35 }
36 t.Setenv("CARLOS_DNSIMPLE_TOKEN", "tok")
37 if p, reason := dnsProviderFromEnv(); p == nil {
38 t.Fatalf("token set: want dnsimple provider, got nil (%s)", reason)
39 } else if _, ok := p.(*dnsimpleDNS); !ok {
40 t.Fatalf("token set: want *dnsimpleDNS, got %T", p)
41 }
42 t.Setenv("CARLOS_DNS_PROVIDER", "bogus")
43 if p, reason := dnsProviderFromEnv(); p != nil || reason == "" {
44 t.Fatalf("bogus provider: want (nil, reason), got (%v, %q)", p, reason)
45 }
46 // route53 selection is exercised in route53dns_test.go — constructing it
47 // here would hit the AWS credential chain.
48}
49
50// fakeDNSimple is a minimal DNSimple v2 API: whoami, per-zone existence,
51// record create/delete. It records mutations for assertions.
52type fakeDNSimple struct {
53 zones map[string]bool // zone name → exists (200)
54 forbidden map[string]bool // zone name → exists but not owned by this account (403)
55 created []string // "zone|name|content"
56 deleted []int64
57 nextID int64
58}
59
60// handler wires the mux and wraps it in an auth-checking middleware so the
61// Authorization header is asserted on every request — whoami, zone lookup,
62// record create, AND record delete — not just whoami.
63func (f *fakeDNSimple) handler(t *testing.T) http.Handler {
64 mux := http.NewServeMux()
65 mux.HandleFunc("GET /v2/whoami", func(w http.ResponseWriter, r *http.Request) {
66 fmt.Fprint(w, `{"data":{"account":{"id":42}}}`)
67 })
68 mux.HandleFunc("GET /v2/42/zones/{zone}", func(w http.ResponseWriter, r *http.Request) {
69 zone := r.PathValue("zone")
70 switch {
71 case f.zones[zone]:
72 fmt.Fprintf(w, `{"data":{"name":%q}}`, zone)
73 case f.forbidden[zone]:
74 // A zone that exists but isn't owned by this account's token:
75 // DNSimple answers 403, not 404.
76 http.Error(w, `{"message":"forbidden"}`, 403)
77 default:
78 http.Error(w, `{"message":"not found"}`, 404)
79 }
80 })
81 mux.HandleFunc("POST /v2/42/zones/{zone}/records", func(w http.ResponseWriter, r *http.Request) {
82 var body struct {
83 Name, Type, Content string
84 TTL int
85 }
86 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
87 t.Errorf("create record body: %v", err)
88 }
89 if body.Type != "TXT" {
90 t.Errorf("create record type = %q, want TXT", body.Type)
91 }
92 if body.TTL != 60 {
93 t.Errorf("create record ttl = %d, want 60", body.TTL)
94 }
95 f.nextID++
96 f.created = append(f.created, fmt.Sprintf("%s|%s|%s", r.PathValue("zone"), body.Name, body.Content))
97 w.WriteHeader(201)
98 fmt.Fprintf(w, `{"data":{"id":%d}}`, f.nextID)
99 })
100 mux.HandleFunc("DELETE /v2/42/zones/{zone}/records/{id}", func(w http.ResponseWriter, r *http.Request) {
101 var id int64
102 fmt.Sscan(r.PathValue("id"), &id)
103 f.deleted = append(f.deleted, id)
104 w.WriteHeader(204)
105 })
106 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107 if r.Header.Get("Authorization") != "Bearer tok" {
108 t.Errorf("%s %s auth header = %q", r.Method, r.URL.Path, r.Header.Get("Authorization"))
109 }
110 mux.ServeHTTP(w, r)
111 })
112}
113
114// The per-account adaptation: the zone is discovered by suffix-walking fqdn
115// against the API, so a challenge under abs.oncarlos.com lands in the
116// oncarlos.com zone with the account label in the record NAME.
117func TestDNSimplePresentDiscoversZone(t *testing.T) {
118 fake := &fakeDNSimple{zones: map[string]bool{"oncarlos.com": true}}
119 srv := httptest.NewServer(fake.handler(t))
120 defer srv.Close()
121 t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL)
122
123 d := newDNSimpleDNS("tok")
124 cleanup, err := d.present(context.Background(), "_acme-challenge.abs.oncarlos.com", "txtvalue")
125 if err != nil {
126 t.Fatalf("present: %v", err)
127 }
128 want := []string{"oncarlos.com|_acme-challenge.abs|txtvalue"}
129 if !reflect.DeepEqual(fake.created, want) {
130 t.Fatalf("created = %v, want %v", fake.created, want)
131 }
132 if err := cleanup(context.Background()); err != nil {
133 t.Fatalf("cleanup: %v", err)
134 }
135 if !reflect.DeepEqual(fake.deleted, []int64{1}) {
136 t.Fatalf("deleted = %v, want [1]", fake.deleted)
137 }
138}
139
140// A 403 means "this zone exists but your token doesn't own it" — DNSimple's
141// real API answers Forbidden, not NotFound, for zones outside the account.
142// The walk must treat that the same as a 404 (keep walking) rather than
143// aborting, or flagship would never discover oncarlos.com if an intermediate
144// suffix candidate happened to be a zone owned by a different account.
145func TestDNSimplePresentSkipsForbiddenAndNotFoundZones(t *testing.T) {
146 fake := &fakeDNSimple{
147 zones: map[string]bool{"oncarlos.com": true},
148 forbidden: map[string]bool{"x.abs.oncarlos.com": true},
149 // "abs.oncarlos.com" is in neither map, so it 404s (not found).
150 }
151 srv := httptest.NewServer(fake.handler(t))
152 defer srv.Close()
153 t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL)
154
155 d := newDNSimpleDNS("tok")
156 cleanup, err := d.present(context.Background(), "_acme-challenge.x.abs.oncarlos.com", "txtvalue")
157 if err != nil {
158 t.Fatalf("present: %v", err)
159 }
160 want := []string{"oncarlos.com|_acme-challenge.x.abs|txtvalue"}
161 if !reflect.DeepEqual(fake.created, want) {
162 t.Fatalf("created = %v, want %v", fake.created, want)
163 }
164 if err := cleanup(context.Background()); err != nil {
165 t.Fatalf("cleanup: %v", err)
166 }
167}
168
169func TestDNSimplePresentNoZone(t *testing.T) {
170 fake := &fakeDNSimple{zones: map[string]bool{}}
171 srv := httptest.NewServer(fake.handler(t))
172 defer srv.Close()
173 t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL)
174
175 d := newDNSimpleDNS("tok")
176 if _, err := d.present(context.Background(), "_acme-challenge.abs.nowhere.example", "v"); err == nil {
177 t.Fatal("present with no owning zone: want error, got nil")
178 }
179}
180
181// checkZone answers from the same suffix walk present uses, writes nothing,
182// and reports a missing zone as errNoZone.
183func TestDNSimpleCheckZone(t *testing.T) {
184 fake := &fakeDNSimple{zones: map[string]bool{"letsjelly.dev": true}}
185 srv := httptest.NewServer(fake.handler(t))
186 defer srv.Close()
187 t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL)
188
189 d := newDNSimpleDNS("tok")
190 if err := d.checkZone(context.Background(), "_acme-challenge.letsjelly.dev"); err != nil {
191 t.Fatalf("held zone: %v", err)
192 }
193 err := d.checkZone(context.Background(), "_acme-challenge.bad.carlos.exciting.io")
194 if !errors.Is(err, errNoZone) {
195 t.Fatalf("missing zone: err = %v, want errNoZone", err)
196 }
197 if len(fake.created) != 0 || len(fake.deleted) != 0 {
198 t.Fatalf("checkZone wrote records: created %v, deleted %v", fake.created, fake.deleted)
199 }
200}
201