You've already forked coredns-powerdns-api-wrapper
175 lines
4.4 KiB
Go
175 lines
4.4 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// newTestRouter builds a WebServer with a minimal Config and wires the real
|
|
// Routes()/middleware stack, without any DB (nil Store/DB) — for the pure
|
|
// handler tests that never reach the Store (auth, unknown server_id, etc).
|
|
func newTestRouter(cfg *Config) (*WebServer, *mux.Router) {
|
|
ws := &WebServer{
|
|
Config: cfg,
|
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
|
}
|
|
r := mux.NewRouter()
|
|
r.Use(ws.loggingMiddleware)
|
|
ws.Routes(r)
|
|
return ws, r
|
|
}
|
|
|
|
func testConfig() *Config {
|
|
return &Config{
|
|
PDNSAPIKey: "test-secret-key",
|
|
ListenAddr: ":3000",
|
|
TablePrefix: "test_pdnsapi_",
|
|
PDNSServerID: "localhost",
|
|
DefaultTTL: 120,
|
|
LogLevel: "info",
|
|
PUID: 1000,
|
|
PGID: 1000,
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware(t *testing.T) {
|
|
cfg := testConfig()
|
|
_, r := newTestRouter(cfg)
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
tests := []struct {
|
|
name string
|
|
headerKey string
|
|
sendHeader bool
|
|
wantStatus int
|
|
wantBody bool
|
|
}{
|
|
{"missing key", "", false, http.StatusUnauthorized, true},
|
|
{"wrong key", "wrong-key", true, http.StatusUnauthorized, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", srv.URL+"/api/v1/servers/localhost/zones", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tt.sendHeader {
|
|
req.Header.Set("X-API-Key", tt.headerKey)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != tt.wantStatus {
|
|
t.Fatalf("status = %d, want %d", resp.StatusCode, tt.wantStatus)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantBody := `{"error":"Unauthorized"}` + "\n"
|
|
if string(body) != wantBody {
|
|
t.Fatalf("body = %q, want %q", string(body), wantBody)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthMiddleware_CorrectKeyPassesThrough(t *testing.T) {
|
|
// Correct key should reach the handler (which will fail past auth since
|
|
// there is no Store — but it must NOT be a 401). Unknown server_id also
|
|
// checked before Store is touched, so use that to prove auth passed.
|
|
cfg := testConfig()
|
|
_, r := newTestRouter(cfg)
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
req, err := http.NewRequest("GET", srv.URL+"/api/v1/servers/not-localhost/zones", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Set("X-API-Key", cfg.PDNSAPIKey)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
t.Fatalf("correct key was rejected with 401")
|
|
}
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d (unknown server_id, proves auth passed)", resp.StatusCode, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestUnknownServerID(t *testing.T) {
|
|
cfg := testConfig()
|
|
_, r := newTestRouter(cfg)
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
paths := []struct {
|
|
method string
|
|
path string
|
|
}{
|
|
{"GET", "/api/v1/servers/bogus/zones"},
|
|
{"GET", "/api/v1/servers/bogus/zones/example.com."},
|
|
{"PATCH", "/api/v1/servers/bogus/zones/example.com."},
|
|
{"PUT", "/api/v1/servers/bogus/zones/example.com./notify"},
|
|
}
|
|
|
|
for _, p := range paths {
|
|
t.Run(p.method+" "+p.path, func(t *testing.T) {
|
|
req, err := http.NewRequest(p.method, srv.URL+p.path, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Set("X-API-Key", cfg.PDNSAPIKey)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want 404", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
want := `{"error":"Not Found"}` + "\n"
|
|
if string(body) != want {
|
|
t.Fatalf("body = %q, want %q", string(body), want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteJSONError(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
writeJSONError(w, http.StatusTeapot, "custom message")
|
|
|
|
if w.Code != http.StatusTeapot {
|
|
t.Fatalf("status = %d, want %d", w.Code, http.StatusTeapot)
|
|
}
|
|
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
|
t.Fatalf("Content-Type = %q, want application/json", ct)
|
|
}
|
|
want := `{"error":"custom message"}` + "\n"
|
|
if w.Body.String() != want {
|
|
t.Fatalf("body = %q, want %q", w.Body.String(), want)
|
|
}
|
|
}
|