Files
tunnel/internal/server/dns_porkbun_test.go
T
TZGyn ebae420ab1 Add the native Go client and finish M3 hardening
The new cmd/tunnel client keeps the API URL and tunnel identity in
~/.config/tunnel/config.json (0600), provisions the key/CSR and
certificate, terminates TLS, proxies to local routes, reloads routes
without a restart, and runs as a background service. `tunnel import`
takes over an existing official CLI profile, preserving its tunnel ID,
token, key and certificate.

Server hardening: per-IP and per-tunnel rate limits for creates and
binds, a CA cool-down after repeated ACME failures, a startup sweep for
stale _acme-challenge records, admin metrics, a 60-second half-close
timeout, and separate prioritized control / ordered data bridge queues
(with `end` ordered after the final bytes). The dashboard shows the new
metrics.
2026-10-26 00:32:31 +08:00

118 lines
3.7 KiB
Go

package server
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPorkbunDNS(t *testing.T) {
var createPath, createBody, deletePath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
switch {
case strings.HasPrefix(r.URL.Path, "/dns/create/"):
createPath, createBody = r.URL.Path, string(body)
w.Write([]byte(`{"status":"SUCCESS","id":"42"}`))
case strings.HasPrefix(r.URL.Path, "/dns/deleteByNameType/"):
deletePath = r.URL.Path
w.Write([]byte(`{"status":"SUCCESS"}`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
p := &PorkbunDNS{APIKey: "pk", SecretKey: "sk", Zone: "tzgyn.dev", BaseURL: srv.URL, HTTP: srv.Client()}
ctx := context.Background()
record := "_acme-challenge.abc123.tunnel.tzgyn.dev"
if err := p.CreateTXT(ctx, record, "challenge-value"); err != nil {
t.Fatal(err)
}
if createPath != "/dns/create/tzgyn.dev" {
t.Errorf("create path = %q", createPath)
}
var body map[string]string
if err := json.Unmarshal([]byte(createBody), &body); err != nil {
t.Fatal(err)
}
for key, want := range map[string]string{
"apikey": "pk", "secretapikey": "sk",
"name": "_acme-challenge.abc123.tunnel", "type": "TXT", "content": "challenge-value",
} {
if body[key] != want {
t.Errorf("create body %s = %q, want %q", key, body[key], want)
}
}
if err := p.DeleteTXT(ctx, record); err != nil {
t.Fatal(err)
}
if deletePath != "/dns/deleteByNameType/tzgyn.dev/TXT/_acme-challenge.abc123.tunnel" {
t.Errorf("delete path = %q", deletePath)
}
// A record outside the zone fails before any request.
if err := p.CreateTXT(ctx, "other.example.com", "v"); err == nil {
t.Error("expected an error for a record outside the zone")
}
// API errors surface.
bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ERROR","message":"nope"}`))
}))
defer bad.Close()
p2 := &PorkbunDNS{APIKey: "pk", SecretKey: "sk", Zone: "tzgyn.dev", BaseURL: bad.URL, HTTP: bad.Client()}
if err := p2.CreateTXT(ctx, record, "v"); err == nil || !strings.Contains(err.Error(), "nope") {
t.Errorf("expected the API error, got %v", err)
}
}
func TestPorkbunSweepChallenges(t *testing.T) {
var deletes []string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.URL.Path == "/dns/retrieve/tzgyn.dev":
w.Write([]byte(`{"status":"SUCCESS","records":[
{"name":"_acme-challenge.a.tunnel.tzgyn.dev","type":"TXT","content":"one"},
{"name":"_acme-challenge.a.tunnel.tzgyn.dev","type":"TXT","content":"two"},
{"name":"_acme-challenge.b.tunnel.tzgyn.dev","type":"TXT","content":"three"},
{"name":"www.tzgyn.dev","type":"TXT","content":"keep"},
{"name":"_acme-challenge.other.example.com","type":"TXT","content":"outside"}
]}`))
case strings.HasPrefix(r.URL.Path, "/dns/deleteByNameType/"):
deletes = append(deletes, r.URL.Path)
w.Write([]byte(`{"status":"SUCCESS"}`))
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
p := &PorkbunDNS{APIKey: "pk", SecretKey: "sk", Zone: "tzgyn.dev", BaseURL: srv.URL, HTTP: srv.Client()}
count, err := p.SweepChallenges(context.Background())
if err != nil {
t.Fatal(err)
}
if count != 2 {
t.Errorf("count = %d, want 2", count)
}
want := []string{
"/dns/deleteByNameType/tzgyn.dev/TXT/_acme-challenge.a.tunnel",
"/dns/deleteByNameType/tzgyn.dev/TXT/_acme-challenge.b.tunnel",
}
if len(deletes) != len(want) {
t.Fatalf("deletes = %v, want %v", deletes, want)
}
for i := range want {
if deletes[i] != want[i] {
t.Errorf("delete[%d] = %q, want %q", i, deletes[i], want[i])
}
}
}