package edge import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "reflect" "testing" ) func TestSplitDomains(t *testing.T) { cases := []struct { in string want []string }{ {"", nil}, {"go.tito.io", []string{"go.tito.io"}}, {" Go.Tito.IO , dev.oncarlos.com ,", []string{"go.tito.io", "dev.oncarlos.com"}}, } for _, c := range cases { if got := splitDomains(c.in); !reflect.DeepEqual(got, c.want) { t.Errorf("splitDomains(%q) = %v, want %v", c.in, got, c.want) } } } func TestDNSProviderSelection(t *testing.T) { t.Setenv("CARLOS_DNS_PROVIDER", "") t.Setenv("CARLOS_DNSIMPLE_TOKEN", "") if p, reason := dnsProviderFromEnv(); p != nil || reason == "" { t.Fatalf("unset env: want (nil, reason), got (%v, %q)", p, reason) } t.Setenv("CARLOS_DNSIMPLE_TOKEN", "tok") if p, reason := dnsProviderFromEnv(); p == nil { t.Fatalf("token set: want dnsimple provider, got nil (%s)", reason) } else if _, ok := p.(*dnsimpleDNS); !ok { t.Fatalf("token set: want *dnsimpleDNS, got %T", p) } t.Setenv("CARLOS_DNS_PROVIDER", "bogus") if p, reason := dnsProviderFromEnv(); p != nil || reason == "" { t.Fatalf("bogus provider: want (nil, reason), got (%v, %q)", p, reason) } // route53 selection is exercised in route53dns_test.go — constructing it // here would hit the AWS credential chain. } // fakeDNSimple is a minimal DNSimple v2 API: whoami, per-zone existence, // record create/delete. It records mutations for assertions. type fakeDNSimple struct { zones map[string]bool // zone name → exists (200) forbidden map[string]bool // zone name → exists but not owned by this account (403) created []string // "zone|name|content" deleted []int64 nextID int64 } // handler wires the mux and wraps it in an auth-checking middleware so the // Authorization header is asserted on every request — whoami, zone lookup, // record create, AND record delete — not just whoami. func (f *fakeDNSimple) handler(t *testing.T) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /v2/whoami", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"data":{"account":{"id":42}}}`) }) mux.HandleFunc("GET /v2/42/zones/{zone}", func(w http.ResponseWriter, r *http.Request) { zone := r.PathValue("zone") switch { case f.zones[zone]: fmt.Fprintf(w, `{"data":{"name":%q}}`, zone) case f.forbidden[zone]: // A zone that exists but isn't owned by this account's token: // DNSimple answers 403, not 404. http.Error(w, `{"message":"forbidden"}`, 403) default: http.Error(w, `{"message":"not found"}`, 404) } }) mux.HandleFunc("POST /v2/42/zones/{zone}/records", func(w http.ResponseWriter, r *http.Request) { var body struct { Name, Type, Content string TTL int } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Errorf("create record body: %v", err) } if body.Type != "TXT" { t.Errorf("create record type = %q, want TXT", body.Type) } if body.TTL != 60 { t.Errorf("create record ttl = %d, want 60", body.TTL) } f.nextID++ f.created = append(f.created, fmt.Sprintf("%s|%s|%s", r.PathValue("zone"), body.Name, body.Content)) w.WriteHeader(201) fmt.Fprintf(w, `{"data":{"id":%d}}`, f.nextID) }) mux.HandleFunc("DELETE /v2/42/zones/{zone}/records/{id}", func(w http.ResponseWriter, r *http.Request) { var id int64 fmt.Sscan(r.PathValue("id"), &id) f.deleted = append(f.deleted, id) w.WriteHeader(204) }) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer tok" { t.Errorf("%s %s auth header = %q", r.Method, r.URL.Path, r.Header.Get("Authorization")) } mux.ServeHTTP(w, r) }) } // The per-account adaptation: the zone is discovered by suffix-walking fqdn // against the API, so a challenge under abs.oncarlos.com lands in the // oncarlos.com zone with the account label in the record NAME. func TestDNSimplePresentDiscoversZone(t *testing.T) { fake := &fakeDNSimple{zones: map[string]bool{"oncarlos.com": true}} srv := httptest.NewServer(fake.handler(t)) defer srv.Close() t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL) d := newDNSimpleDNS("tok") cleanup, err := d.present(context.Background(), "_acme-challenge.abs.oncarlos.com", "txtvalue") if err != nil { t.Fatalf("present: %v", err) } want := []string{"oncarlos.com|_acme-challenge.abs|txtvalue"} if !reflect.DeepEqual(fake.created, want) { t.Fatalf("created = %v, want %v", fake.created, want) } if err := cleanup(context.Background()); err != nil { t.Fatalf("cleanup: %v", err) } if !reflect.DeepEqual(fake.deleted, []int64{1}) { t.Fatalf("deleted = %v, want [1]", fake.deleted) } } // A 403 means "this zone exists but your token doesn't own it" — DNSimple's // real API answers Forbidden, not NotFound, for zones outside the account. // The walk must treat that the same as a 404 (keep walking) rather than // aborting, or flagship would never discover oncarlos.com if an intermediate // suffix candidate happened to be a zone owned by a different account. func TestDNSimplePresentSkipsForbiddenAndNotFoundZones(t *testing.T) { fake := &fakeDNSimple{ zones: map[string]bool{"oncarlos.com": true}, forbidden: map[string]bool{"x.abs.oncarlos.com": true}, // "abs.oncarlos.com" is in neither map, so it 404s (not found). } srv := httptest.NewServer(fake.handler(t)) defer srv.Close() t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL) d := newDNSimpleDNS("tok") cleanup, err := d.present(context.Background(), "_acme-challenge.x.abs.oncarlos.com", "txtvalue") if err != nil { t.Fatalf("present: %v", err) } want := []string{"oncarlos.com|_acme-challenge.x.abs|txtvalue"} if !reflect.DeepEqual(fake.created, want) { t.Fatalf("created = %v, want %v", fake.created, want) } if err := cleanup(context.Background()); err != nil { t.Fatalf("cleanup: %v", err) } } func TestDNSimplePresentNoZone(t *testing.T) { fake := &fakeDNSimple{zones: map[string]bool{}} srv := httptest.NewServer(fake.handler(t)) defer srv.Close() t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL) d := newDNSimpleDNS("tok") if _, err := d.present(context.Background(), "_acme-challenge.abs.nowhere.example", "v"); err == nil { t.Fatal("present with no owning zone: want error, got nil") } } // checkZone answers from the same suffix walk present uses, writes nothing, // and reports a missing zone as errNoZone. func TestDNSimpleCheckZone(t *testing.T) { fake := &fakeDNSimple{zones: map[string]bool{"letsjelly.dev": true}} srv := httptest.NewServer(fake.handler(t)) defer srv.Close() t.Setenv("CARLOS_DNSIMPLE_BASE", srv.URL) d := newDNSimpleDNS("tok") if err := d.checkZone(context.Background(), "_acme-challenge.letsjelly.dev"); err != nil { t.Fatalf("held zone: %v", err) } err := d.checkZone(context.Background(), "_acme-challenge.bad.carlos.exciting.io") if !errors.Is(err, errNoZone) { t.Fatalf("missing zone: err = %v, want errNoZone", err) } if len(fake.created) != 0 || len(fake.deleted) != 0 { t.Fatalf("checkZone wrote records: created %v, deleted %v", fake.created, fake.deleted) } }