Files
pdns-admin/internal/server/server_test.go

324 lines
8.5 KiB
Go

package server
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"pdns_admin/internal/pdns"
)
type fakeClient struct {
deletedRRSetType string
createdRRSet pdns.RRSet
}
type fakeAuth struct {
allowed bool
err error
username string
password string
}
func (f *fakeAuth) Authenticate(_ context.Context, username, password string) (bool, error) {
f.username = username
f.password = password
return f.allowed, f.err
}
func (fakeClient) GetServer(context.Context) (pdns.Server, error) {
return pdns.Server{}, nil
}
func (fakeClient) ListZones(context.Context) ([]pdns.Zone, error) {
return nil, nil
}
func (fakeClient) CreateZone(context.Context, pdns.Zone) (pdns.Zone, error) {
return pdns.Zone{}, nil
}
func (fakeClient) DeleteZone(context.Context, string) error {
return nil
}
func (fakeClient) GetZone(context.Context, string) (pdns.Zone, error) {
return pdns.Zone{}, nil
}
func (f *fakeClient) CreateRRSet(_ context.Context, _ string, rrset pdns.RRSet) error {
f.createdRRSet = rrset
return nil
}
func (f *fakeClient) DeleteRRSet(_ context.Context, _, _, recordType string) error {
f.deletedRRSetType = recordType
return nil
}
func TestNewRequiresClient(t *testing.T) {
if _, err := New(Config{}, nil, nil); err == nil {
t.Fatal("expected error for missing client")
}
}
func TestNewBuildsServer(t *testing.T) {
srv, err := New(Config{Addr: ":0"}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
if srv.addr != ":0" {
t.Fatalf("unexpected addr: %s", srv.addr)
}
}
func TestAuthCanBeDisabled(t *testing.T) {
srv, err := New(Config{}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("unexpected status: %d", rec.Code)
}
}
func TestServesEmbeddedTablerAssets(t *testing.T) {
srv, err := New(Config{}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/static/vendor/tabler.min.css", nil)
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("unexpected status: %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "Tabler v1.4.0") {
t.Fatal("expected vendored Tabler CSS")
}
}
func TestAuthRedirectsProtectedRoutesToLogin(t *testing.T) {
srv, err := New(Config{Authenticator: &fakeAuth{allowed: true}}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/zones", nil)
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("unexpected status: %d", rec.Code)
}
if got := rec.Header().Get("Location"); got != "/login?next=%2Fzones" {
t.Fatalf("unexpected location: %q", got)
}
}
func TestLoginCreatesSession(t *testing.T) {
auth := &fakeAuth{allowed: true}
srv, err := New(Config{Authenticator: auth}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
body := strings.NewReader("username=alice&password=secret&next=/zones")
req := httptest.NewRequest(http.MethodPost, "/login", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("unexpected status: %d", rec.Code)
}
if auth.username != "alice" || auth.password != "secret" {
t.Fatalf("unexpected credentials: %q %q", auth.username, auth.password)
}
cookies := rec.Result().Cookies()
if len(cookies) == 0 || cookies[0].Name != sessionCookieName {
t.Fatalf("expected session cookie, got %#v", cookies)
}
if !cookies[0].HttpOnly {
t.Fatal("session cookie must be HttpOnly")
}
if !cookies[0].Secure {
t.Fatal("session cookie must be Secure")
}
if cookies[0].SameSite != http.SameSiteStrictMode {
t.Fatalf("unexpected SameSite policy: %v", cookies[0].SameSite)
}
req = httptest.NewRequest(http.MethodGet, "/zones", nil)
req.AddCookie(cookies[0])
rec = httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("unexpected authenticated status: %d", rec.Code)
}
}
func TestLoginReportsBackendErrors(t *testing.T) {
srv, err := New(Config{Authenticator: &fakeAuth{err: errors.New("ldap down")}}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
body := strings.NewReader("username=alice&password=secret")
req := httptest.NewRequest(http.MethodPost, "/login", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("unexpected status: %d", rec.Code)
}
if !strings.HasPrefix(rec.Header().Get("Location"), "/login?error=") {
t.Fatalf("unexpected location: %q", rec.Header().Get("Location"))
}
}
func TestLogoutClearsSession(t *testing.T) {
srv, err := New(Config{Authenticator: &fakeAuth{allowed: true}}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
token, err := srv.createSession("alice")
if err != nil {
t.Fatalf("createSession returned error: %v", err)
}
csrfToken := srv.sessions[token].CSRFToken
body := strings.NewReader("csrf_token=" + csrfToken)
req := httptest.NewRequest(http.MethodPost, "/logout", body)
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: token})
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("unexpected status: %d", rec.Code)
}
if _, ok := srv.sessions[token]; ok {
t.Fatal("session was not deleted")
}
}
func TestLogoutRequiresCSRF(t *testing.T) {
srv, err := New(Config{Authenticator: &fakeAuth{allowed: true}}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
token, err := srv.createSession("alice")
if err != nil {
t.Fatalf("createSession returned error: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: token})
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("unexpected status: %d", rec.Code)
}
}
func TestSecurityHeadersAreSet(t *testing.T) {
srv, err := New(Config{}, &fakeClient{}, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
for _, header := range []string{
"Content-Security-Policy",
"Referrer-Policy",
"Strict-Transport-Security",
"X-Content-Type-Options",
"X-Frame-Options",
} {
if rec.Header().Get(header) == "" {
t.Fatalf("expected %s header", header)
}
}
}
func TestParseLinesSkipsBlankLines(t *testing.T) {
values := parseLines("192.0.2.1\n\n192.0.2.2\n")
if len(values) != 2 {
t.Fatalf("unexpected values: %#v", values)
}
}
func TestDeleteRRSetRejectsSOA(t *testing.T) {
client := &fakeClient{}
srv, err := New(Config{}, client, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
body := strings.NewReader("name=example.org.&type=SOA")
req := httptest.NewRequest(http.MethodPost, "/zones/example.org./rrsets/delete", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("unexpected status: %d", rec.Code)
}
if client.deletedRRSetType != "" {
t.Fatalf("SOA delete reached client")
}
}
func TestSaveEditedRRSetUsesQueryIdentity(t *testing.T) {
client := &fakeClient{}
srv, err := New(Config{}, client, nil)
if err != nil {
t.Fatalf("New returned error: %v", err)
}
body := strings.NewReader("name=evil.example.org.&type=AAAA&ttl=300&records=192.0.2.10")
req := httptest.NewRequest(http.MethodPost, "/zones/example.org./rrsets/edit?name=www.example.org.&type=A", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()
srv.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusSeeOther {
t.Fatalf("unexpected status: %d", rec.Code)
}
if client.createdRRSet.Name != "www.example.org." {
t.Fatalf("name came from form: %#v", client.createdRRSet)
}
if client.createdRRSet.Type != "A" {
t.Fatalf("type came from form: %#v", client.createdRRSet)
}
}