| index 485b06046..bc3a318cc 100644 |
| --- a/domains_cli_test.go |
| +++ b/domains_cli_test.go |
| @@ -38,7 +38,7 @@ func TestAPISessionDomainAttach(t *testing.T) { |
| s := newTestSession(ts.URL) |
| |
| created, route, err := s.domainAttach(context.Background(), apiAcct{Seg: "bab"}, "tito", |
| - "tickets.example.com", "slug.go.tito.io") |
| + "tickets.example.com", "slug.go.tito.io", nil, nil) |
| if err != nil { |
| t.Fatalf("domainAttach: %v", err) |
| } |
| @@ -66,7 +66,7 @@ func TestAPISessionDomainAttachNoOp(t *testing.T) { |
| s := newTestSession(ts.URL) |
| |
| created, route, err := s.domainAttach(context.Background(), apiAcct{Seg: "bab"}, "tito", |
| - "tickets.example.com", "") |
| + "tickets.example.com", "", nil, nil) |
| if err != nil { |
| t.Fatalf("domainAttach: %v", err) |
| } |
| @@ -86,12 +86,73 @@ func TestAPISessionDomainAttachConflict(t *testing.T) { |
| defer ts.Close() |
| s := newTestSession(ts.URL) |
| |
| - _, _, err := s.domainAttach(context.Background(), apiAcct{Seg: "bab"}, "tito", "taken.example.com", "") |
| + _, _, err := s.domainAttach(context.Background(), apiAcct{Seg: "bab"}, "tito", "taken.example.com", "", nil, nil) |
| if err == nil || err.Error() != "hostname unavailable" { |
| t.Fatalf("domainAttach against 409 = %v, want exactly \"hostname unavailable\"", err) |
| } |
| } |
| |
| +// TestAPISessionDomainAttachWildcardRefusal: a 422 (the wildcard/catchall |
| +// gate) surfaces its own message verbatim too — the CNAME record the caller |
| +// needs to create, exactly as the door wrote it, not summarized. |
| +func TestAPISessionDomainAttachWildcardRefusal(t *testing.T) { |
| + const msg = "create CNAME _acme-challenge.tickets.example.com -> _acme-challenge.tickets.example.com.acme.oncarlos.com, then retry" |
| + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| + w.WriteHeader(http.StatusUnprocessableEntity) |
| + json.NewEncoder(w).Encode(map[string]any{"error": msg}) |
| + })) |
| + defer ts.Close() |
| + s := newTestSession(ts.URL) |
| + |
| + yes := true |
| + _, _, err := s.domainAttach(context.Background(), apiAcct{Seg: "bab"}, "tito", "tickets.example.com", "", &yes, nil) |
| + if err == nil || err.Error() != msg { |
| + t.Fatalf("domainAttach against a 422 = %v, want exactly %q", err, msg) |
| + } |
| +} |
| + |
| +// TestAPISessionDomainAttachWildcardCatchallWireShape: wildcard/catchall |
| +// pointers marshal only when non-nil — nil never reaches the wire at all |
| +// (omitempty on a nil *bool), so a caller that names neither sends a body |
| +// indistinguishable from the pre-Task-5 shape. |
| +func TestAPISessionDomainAttachWildcardCatchallWireShape(t *testing.T) { |
| + var gotBody map[string]json.RawMessage |
| + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| + json.NewDecoder(r.Body).Decode(&gotBody) |
| + json.NewEncoder(w).Encode(map[string]any{"host": "tickets.example.com", "route": "go.tito.io", "created": true}) |
| + })) |
| + defer ts.Close() |
| + s := newTestSession(ts.URL) |
| + |
| + // Neither named: the wire body must carry no "wildcard"/"catchall" keys |
| + // at all, not false-valued ones — an absent key is what "leave |
| + // unchanged" means to the door. |
| + if _, _, err := s.domainAttach(context.Background(), apiAcct{Seg: "bab"}, "tito", "tickets.example.com", "", nil, nil); err != nil { |
| + t.Fatalf("domainAttach: %v", err) |
| + } |
| + if _, ok := gotBody["wildcard"]; ok { |
| + t.Fatalf("body = %v, want no wildcard key when nil", gotBody) |
| + } |
| + if _, ok := gotBody["catchall"]; ok { |
| + t.Fatalf("body = %v, want no catchall key when nil", gotBody) |
| + } |
| + |
| + // wildcard named true, catchall named false: both keys present, with |
| + // their own values — false is a real request ("turn it off"), not an |
| + // absence. |
| + yes, no := true, false |
| + if _, _, err := s.domainAttach(context.Background(), apiAcct{Seg: "bab"}, "tito", "tickets.example.com", "", &yes, &no); err != nil { |
| + t.Fatalf("domainAttach: %v", err) |
| + } |
| + var w, c bool |
| + if err := json.Unmarshal(gotBody["wildcard"], &w); err != nil || !w { |
| + t.Fatalf("wildcard = %v (err %v), want true", gotBody["wildcard"], err) |
| + } |
| + if err := json.Unmarshal(gotBody["catchall"], &c); err != nil || c { |
| + t.Fatalf("catchall = %v (err %v), want false", gotBody["catchall"], err) |
| + } |
| +} |
| + |
| // TestAPISessionDomainDetach: domainDetach DELETEs with the host |
| // path-escaped and decodes removed. |
| func TestAPISessionDomainDetach(t *testing.T) { |
| @@ -185,7 +246,7 @@ func TestAPISessionDomainDoorsSurfaceServerErrors(t *testing.T) { |
| s := newTestSession(ts.URL) |
| acct := apiAcct{Seg: "bab"} |
| |
| - if _, _, err := s.domainAttach(context.Background(), acct, "ghost", "x.example.com", ""); err == nil { |
| + if _, _, err := s.domainAttach(context.Background(), acct, "ghost", "x.example.com", "", nil, nil); err == nil { |
| t.Fatal("domainAttach against a 404 must error") |
| } |
| if _, err := s.domainDetach(context.Background(), acct, "ghost", "x.example.com"); err == nil { |
| @@ -203,7 +264,10 @@ func TestAPISessionDomainDoorsSurfaceServerErrors(t *testing.T) { |
| type fakeDomainsConsole struct { |
| ts *httptest.Server |
| lastAttachPath string |
| - lastAttachBody struct{ Host, Route string } |
| + lastAttachBody struct { |
| + Host, Route string |
| + Wildcard, Catchall *bool |
| + } |
| attachCreated bool |
| lastDetachPath string |
| detachRemoved bool |
| @@ -231,8 +295,10 @@ func newFakeDomainsConsole(t *testing.T) *fakeDomainsConsole { |
| json.NewEncoder(w).Encode(map[string]any{ |
| "domains": []map[string]any{ |
| {"host": "tickets.example.com", "route": "slug.go.tito.io", "status": "live", |
| - "dns_status": "pointing", "cert_expires_at": 1799999999}, |
| - {"host": "new.example.com", "route": "slug.go.tito.io", "status": "pending"}, |
| + "dns_status": "pointing", "cert_expires_at": 1799999999, |
| + "wildcard": true, "catchall": true, "delegation_status": "delegated"}, |
| + {"host": "new.example.com", "route": "slug.go.tito.io", "status": "pending", |
| + "wildcard": false, "catchall": false}, |
| }, |
| }) |
| default: |
| @@ -324,7 +390,7 @@ func TestRunDomainsListPrintsTable(t *testing.T) { |
| |
| out := buf.String() |
| for _, want := range []string{ |
| - "HOST", "ROUTE", "STATUS", "DNS", "CERT-EXPIRES", |
| + "HOST", "ROUTE", "STATUS", "DNS", "CERT-EXPIRES", "WILDCARD", "CATCHALL", "DELEGATION", |
| "tickets.example.com", "slug.go.tito.io", "live", "pointing", "2027-01-15", |
| "new.example.com", "pending", |
| } { |
| @@ -332,17 +398,76 @@ func TestRunDomainsListPrintsTable(t *testing.T) { |
| t.Fatalf("stdout missing %q:\n%s", want, out) |
| } |
| } |
| - // The pending row's dns/cert columns must read "-", not a stray zero or |
| - // blank cell that would misalign the table. |
| lines := strings.Split(out, "\n") |
| - var pendingLine string |
| + var wildcardLine, plainLine string |
| for _, l := range lines { |
| + if strings.Contains(l, "tickets.example.com") { |
| + wildcardLine = l |
| + } |
| if strings.Contains(l, "new.example.com") { |
| - pendingLine = l |
| + plainLine = l |
| } |
| } |
| - if pendingLine == "" || !strings.Contains(pendingLine, "-") { |
| - t.Fatalf("pending row = %q, want a \"-\" placeholder for the absent columns", pendingLine) |
| + // tickets.example.com carries wildcard+catchall+delegated: "yes" twice |
| + // and "delegated" once, in the last three (tabwriter-padded) columns. |
| + if wildcardLine == "" { |
| + t.Fatal("no row for tickets.example.com") |
| + } |
| + if fields := strings.Fields(wildcardLine); len(fields) < 3 || |
| + fields[len(fields)-3] != "yes" || fields[len(fields)-2] != "yes" || fields[len(fields)-1] != "delegated" { |
| + t.Fatalf("wildcard row = %q, want yes/yes/delegated in the last three columns", wildcardLine) |
| + } |
| + // The pending row's dns/cert columns read "-", and wildcard/catchall are |
| + // blank (not "yes") with delegation "-" — the door omits the field. |
| + if plainLine == "" { |
| + t.Fatal("no row for new.example.com") |
| + } |
| + if fields := strings.Fields(plainLine); len(fields) < 1 || fields[len(fields)-1] != "-" { |
| + t.Fatalf("pending row = %q, want delegation \"-\"", plainLine) |
| + } |
| + if strings.Contains(plainLine, "yes") { |
| + t.Fatalf("pending row = %q, want no wildcard/catchall yes when both are false", plainLine) |
| + } |
| +} |
| + |
| +// TestRunDomainsAttachWildcardFlagSentOnlyWhenNamed: -wildcard/-catchall are |
| +// tri-state on the wire, the way -target already is on `apps place` — |
| +// fs.Visit is what makes "not named" and "named false" distinguishable, and |
| +// this is the regression that would silently break if runDomainsAttach ever |
| +// switched to reading *wildcard/*catchall unconditionally. |
| +func TestRunDomainsAttachWildcardFlagSentOnlyWhenNamed(t *testing.T) { |
| + fc := domainsCLIFixture(t) |
| + |
| + // Bare attach: neither flag named, neither pointer sent. |
| + runDomains([]string{"attach", "-app", "tito", "tickets.example.com"}) |
| + if fc.lastAttachBody.Wildcard != nil || fc.lastAttachBody.Catchall != nil { |
| + t.Fatalf("bare attach body = %+v, want both wildcard and catchall unset", fc.lastAttachBody) |
| + } |
| + |
| + // -wildcard named (implicitly true): wildcard sent true, catchall still |
| + // unset — naming one flag must not touch the other. |
| + runDomains([]string{"attach", "-app", "tito", "-wildcard", "tickets.example.com"}) |
| + if fc.lastAttachBody.Wildcard == nil || !*fc.lastAttachBody.Wildcard { |
| + t.Fatalf("wildcard body = %+v, want wildcard=true", fc.lastAttachBody) |
| + } |
| + if fc.lastAttachBody.Catchall != nil { |
| + t.Fatalf("wildcard body = %+v, want catchall still unset", fc.lastAttachBody) |
| + } |
| + |
| + // -wildcard=false named explicitly: a real request to turn it off, sent |
| + // as a present false, not treated as "not named". |
| + runDomains([]string{"attach", "-app", "tito", "-wildcard=false", "tickets.example.com"}) |
| + if fc.lastAttachBody.Wildcard == nil || *fc.lastAttachBody.Wildcard { |
| + t.Fatalf("wildcard=false body = %+v, want wildcard present and false", fc.lastAttachBody) |
| + } |
| + |
| + // Both named: both sent. |
| + runDomains([]string{"attach", "-app", "tito", "-wildcard", "-catchall", "tickets.example.com"}) |
| + if fc.lastAttachBody.Wildcard == nil || !*fc.lastAttachBody.Wildcard { |
| + t.Fatalf("both body = %+v, want wildcard=true", fc.lastAttachBody) |
| + } |
| + if fc.lastAttachBody.Catchall == nil || !*fc.lastAttachBody.Catchall { |
| + t.Fatalf("both body = %+v, want catchall=true", fc.lastAttachBody) |
| } |
| } |
| |