package provider_test import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "sync" "testing" ) const ( testAPIKey = "pk1_test" testSecretKey = "sk1_test" ) type fakeRecord struct { name string typ string content string ttl int prio int } type fakePorkbun struct { t *testing.T mu sync.Mutex nextID int records map[string]map[string]*fakeRecord createCalls, editCalls, deleteCalls, retrieveCalls int } func newFakePorkbun(t *testing.T) *fakePorkbun { return &fakePorkbun{t: t, nextID: 1000, records: map[string]map[string]*fakeRecord{}} } func (f *fakePorkbun) seed(domain string, rec fakeRecord) string { f.mu.Lock() defer f.mu.Unlock() f.nextID++ id := strconv.Itoa(f.nextID) if f.records[domain] == nil { f.records[domain] = map[string]*fakeRecord{} } f.records[domain][id] = &rec return id } func (f *fakePorkbun) record(domain, id string) *fakeRecord { f.mu.Lock() defer f.mu.Unlock() rec, ok := f.records[domain][id] if !ok { return nil } cp := *rec return &cp } func (f *fakePorkbun) recordCount() int { f.mu.Lock() defer f.mu.Unlock() n := 0 for _, recs := range f.records { n += len(recs) } return n } func (f *fakePorkbun) ServeHTTP(w http.ResponseWriter, r *http.Request) { f.mu.Lock() defer f.mu.Unlock() var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { f.fail(w, http.StatusBadRequest, "Invalid JSON.") return } if body["apikey"] != testAPIKey || body["secretapikey"] != testSecretKey { f.fail(w, http.StatusForbidden, "Invalid API key. (002)") return } parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") switch { case len(parts) == 1 && parts[0] == "ping": f.ok(w, map[string]any{"yourIp": "203.0.113.7"}) case len(parts) == 3 && parts[0] == "dns" && parts[1] == "create": f.createCalls++ domain := parts[2] f.nextID++ id := strconv.Itoa(f.nextID) if f.records[domain] == nil { f.records[domain] = map[string]*fakeRecord{} } f.records[domain][id] = &fakeRecord{ name: str(body["name"]), typ: str(body["type"]), content: str(body["content"]), ttl: atoi(f.t, str(body["ttl"])), prio: atoi(f.t, str(body["prio"])), } f.ok(w, map[string]any{"id": f.nextID}) case len(parts) == 4 && parts[0] == "dns" && parts[1] == "edit": f.editCalls++ rec, ok := f.records[parts[2]][parts[3]] if !ok { f.fail(w, http.StatusBadRequest, "Invalid record ID.") return } rec.typ = str(body["type"]) rec.content = str(body["content"]) rec.ttl = atoi(f.t, str(body["ttl"])) rec.prio = atoi(f.t, str(body["prio"])) f.ok(w, nil) case len(parts) == 4 && parts[0] == "dns" && parts[1] == "delete": f.deleteCalls++ if _, ok := f.records[parts[2]][parts[3]]; !ok { f.fail(w, http.StatusBadRequest, "Invalid record ID.") return } delete(f.records[parts[2]], parts[3]) f.ok(w, nil) case len(parts) == 4 && parts[0] == "dns" && parts[1] == "retrieve": f.retrieveCalls++ domain, id := parts[2], parts[3] records := []map[string]any{} if rec, ok := f.records[domain][id]; ok { fullName := domain if rec.name != "" { fullName = rec.name + "." + domain } records = append(records, map[string]any{ "id": id, "name": fullName, "type": rec.typ, "content": rec.content, "ttl": strconv.Itoa(rec.ttl), "prio": strconv.Itoa(rec.prio), "notes": "", }) } f.ok(w, map[string]any{"records": records}) default: f.fail(w, http.StatusNotFound, fmt.Sprintf("Unknown endpoint %q.", r.URL.Path)) } } func (f *fakePorkbun) ok(w http.ResponseWriter, extra map[string]any) { resp := map[string]any{"status": "SUCCESS"} for k, v := range extra { resp[k] = v } json.NewEncoder(w).Encode(resp) } func (f *fakePorkbun) fail(w http.ResponseWriter, code int, message string) { w.WriteHeader(code) json.NewEncoder(w).Encode(map[string]any{"status": "ERROR", "message": message}) } func str(v any) string { s, _ := v.(string) return s } func atoi(t *testing.T, s string) int { t.Helper() if s == "" { return 0 } n, err := strconv.Atoi(s) if err != nil { t.Errorf("fake porkbun: expected numeric string, got %q", s) } return n }