Add comprehensive test suite

This commit is contained in:
2026-02-01 17:53:03 -05:00
parent c5279243c0
commit 63c3c10f2b
8 changed files with 1696 additions and 15 deletions

View File

@@ -0,0 +1,372 @@
package config
import (
"os"
"testing"
)
func TestLoad(t *testing.T) {
// Clean up any existing env vars
cleanup := cleanEnv()
defer cleanup()
// Set test values
os.Setenv("TECHNITIUM_URL", "https://test.dns.example.com")
os.Setenv("TECHNITIUM_TOKEN", "test-token-12345")
os.Setenv("BASE_DOMAIN", "test.rip")
os.Setenv("SPACE_SUBDOMAIN", "dyn")
os.Setenv("RATE_LIMIT_PER_IP", "20")
os.Setenv("RATE_LIMIT_PER_TOKEN", "5")
cfg := Load()
tests := []struct {
name string
got string
expected string
}{
{"TechnitiumURL", cfg.TechnitiumURL, "https://test.dns.example.com"},
{"TechnitiumToken", cfg.TechnitiumToken, "test-token-12345"},
{"BaseDomain", cfg.BaseDomain, "test.rip"},
{"SpaceSubdomain", cfg.SpaceSubdomain, "dyn"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected)
}
})
}
// Test numeric values
if cfg.RateLimitPerIP != 20 {
t.Errorf("RateLimitPerIP = %v, want 20", cfg.RateLimitPerIP)
}
if cfg.RateLimitPerToken != 5 {
t.Errorf("RateLimitPerToken = %v, want 5", cfg.RateLimitPerToken)
}
}
func TestLoadDefaults(t *testing.T) {
cleanup := cleanEnv()
defer cleanup()
cfg := Load()
tests := []struct {
name string
got string
expected string
}{
{"ServerPort", cfg.ServerPort, "8080"},
{"DatabasePath", cfg.DatabasePath, "./dyn.db"},
{"BaseDomain", cfg.BaseDomain, "dws.rip"},
{"SpaceSubdomain", cfg.SpaceSubdomain, "space"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("%s = %v, want %v", tt.name, tt.got, tt.expected)
}
})
}
// Test default numeric values
if cfg.RateLimitPerIP != 10 {
t.Errorf("RateLimitPerIP default = %v, want 10", cfg.RateLimitPerIP)
}
if cfg.RateLimitPerToken != 1 {
t.Errorf("RateLimitPerToken default = %v, want 1", cfg.RateLimitPerToken)
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
setupEnv func()
wantErrors int
}{
{
name: "valid config with token",
setupEnv: func() {
os.Setenv("TECHNITIUM_URL", "https://dns.example.com")
os.Setenv("TECHNITIUM_TOKEN", "valid-token")
},
wantErrors: 0,
},
{
name: "valid config with username/password",
setupEnv: func() {
os.Setenv("TECHNITIUM_URL", "https://dns.example.com")
os.Setenv("TECHNITIUM_USERNAME", "admin")
os.Setenv("TECHNITIUM_PASSWORD", "secret")
os.Unsetenv("TECHNITIUM_TOKEN")
},
wantErrors: 0,
},
{
name: "missing url",
setupEnv: func() {
os.Unsetenv("TECHNITIUM_URL")
os.Setenv("TECHNITIUM_TOKEN", "token")
},
wantErrors: 1,
},
{
name: "missing auth",
setupEnv: func() {
os.Setenv("TECHNITIUM_URL", "https://dns.example.com")
os.Unsetenv("TECHNITIUM_TOKEN")
os.Unsetenv("TECHNITIUM_USERNAME")
os.Unsetenv("TECHNITIUM_PASSWORD")
},
wantErrors: 1,
},
{
name: "missing everything",
setupEnv: func() {
os.Unsetenv("TECHNITIUM_URL")
os.Unsetenv("TECHNITIUM_TOKEN")
os.Unsetenv("TECHNITIUM_USERNAME")
os.Unsetenv("TECHNITIUM_PASSWORD")
},
wantErrors: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cleanup := cleanEnv()
defer cleanup()
tt.setupEnv()
cfg := Load()
errors := cfg.Validate()
if len(errors) != tt.wantErrors {
t.Errorf("Validate() returned %d errors, want %d: %v", len(errors), tt.wantErrors, errors)
}
})
}
}
func TestGetZone(t *testing.T) {
tests := []struct {
name string
base string
space string
expected string
}{
{
name: "with space subdomain",
base: "dws.rip",
space: "space",
expected: "space.dws.rip",
},
{
name: "without space subdomain",
base: "dws.rip",
space: "",
expected: "dws.rip",
},
{
name: "nested subdomain",
base: "example.com",
space: "dyn.space",
expected: "dyn.space.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
BaseDomain: tt.base,
SpaceSubdomain: tt.space,
}
got := cfg.GetZone()
if got != tt.expected {
t.Errorf("GetZone() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetEnv(t *testing.T) {
tests := []struct {
name string
key string
value string
defaultValue string
expected string
}{
{
name: "env set",
key: "TEST_KEY",
value: "test-value",
defaultValue: "default",
expected: "test-value",
},
{
name: "env not set",
key: "UNSET_TEST_KEY",
value: "",
defaultValue: "default",
expected: "default",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.value != "" {
os.Setenv(tt.key, tt.value)
defer os.Unsetenv(tt.key)
}
got := getEnv(tt.key, tt.defaultValue)
if got != tt.expected {
t.Errorf("getEnv() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetEnvAsInt(t *testing.T) {
tests := []struct {
name string
value string
defaultValue int
expected int
}{
{
name: "valid int",
value: "42",
defaultValue: 10,
expected: 42,
},
{
name: "invalid int",
value: "not-a-number",
defaultValue: 10,
expected: 10,
},
{
name: "empty value",
value: "",
defaultValue: 10,
expected: 10,
},
{
name: "negative int",
value: "-5",
defaultValue: 10,
expected: -5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := "TEST_INT_KEY"
if tt.value != "" {
os.Setenv(key, tt.value)
defer os.Unsetenv(key)
}
got := getEnvAsInt(key, tt.defaultValue)
if got != tt.expected {
t.Errorf("getEnvAsInt() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetEnvAsSlice(t *testing.T) {
tests := []struct {
name string
value string
defaultValue []string
expected []string
}{
{
name: "single value",
value: "10.0.0.0/8",
defaultValue: []string{},
expected: []string{"10.0.0.0/8"},
},
{
name: "multiple values",
value: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16",
defaultValue: []string{},
expected: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
},
{
name: "empty value",
value: "",
defaultValue: []string{"default"},
expected: []string{"default"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := "TEST_SLICE_KEY"
if tt.value != "" {
os.Setenv(key, tt.value)
defer os.Unsetenv(key)
}
got := getEnvAsSlice(key, tt.defaultValue)
if len(got) != len(tt.expected) {
t.Errorf("getEnvAsSlice() length = %v, want %v", len(got), len(tt.expected))
return
}
for i, v := range got {
if v != tt.expected[i] {
t.Errorf("getEnvAsSlice()[%d] = %v, want %v", i, v, tt.expected[i])
}
}
})
}
}
// cleanEnv removes all DYN-related env vars and returns a cleanup function
func cleanEnv() func() {
vars := []string{
"SERVER_PORT",
"DATABASE_PATH",
"TECHNITIUM_URL",
"TECHNITIUM_USERNAME",
"TECHNITIUM_PASSWORD",
"TECHNITIUM_TOKEN",
"BASE_DOMAIN",
"SPACE_SUBDOMAIN",
"RATE_LIMIT_PER_IP",
"RATE_LIMIT_PER_TOKEN",
"TRUSTED_PROXIES",
}
// Store old values
oldValues := make(map[string]string)
for _, v := range vars {
if val, ok := os.LookupEnv(v); ok {
oldValues[v] = val
os.Unsetenv(v)
}
}
// Return cleanup function
return func() {
for _, v := range vars {
os.Unsetenv(v)
}
for v, val := range oldValues {
os.Setenv(v, val)
}
}
}

View File

@@ -0,0 +1,353 @@
package database
import (
"context"
"os"
"testing"
"time"
"git.dws.rip/DWS/dyn/internal/models"
)
func setupTestDB(t *testing.T) (*DB, func()) {
tmpFile := "/tmp/test_dyn_" + time.Now().Format("20060102150405") + ".db"
db, err := New(tmpFile)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
cleanup := func() {
db.Close()
os.Remove(tmpFile)
}
return db, cleanup
}
func TestCreateSpace(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
tests := []struct {
name string
subdomain string
wantErr bool
}{
{
name: "create valid space",
subdomain: "myhome",
wantErr: false,
},
{
name: "create another valid space",
subdomain: "office",
wantErr: false,
},
{
name: "duplicate subdomain",
subdomain: "myhome",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
space, err := db.CreateSpace(ctx, tt.subdomain)
if tt.wantErr {
if err == nil {
t.Errorf("CreateSpace() expected error but got none")
}
return
}
if err != nil {
t.Errorf("CreateSpace() unexpected error: %v", err)
return
}
if space == nil {
t.Errorf("CreateSpace() returned nil space")
return
}
if space.Subdomain != tt.subdomain {
t.Errorf("CreateSpace() subdomain = %v, want %v", space.Subdomain, tt.subdomain)
}
if space.Token == "" {
t.Errorf("CreateSpace() token is empty")
}
})
}
}
func TestGetSpaceByToken(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
created, err := db.CreateSpace(ctx, "testspace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
token string
wantNil bool
}{
{
name: "existing space",
token: created.Token,
wantNil: false,
},
{
name: "non-existent token",
token: "invalid-token-12345",
wantNil: true,
},
{
name: "empty token",
token: "",
wantNil: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
space, err := db.GetSpaceByToken(ctx, tt.token)
if err != nil {
t.Errorf("GetSpaceByToken() unexpected error: %v", err)
return
}
if tt.wantNil && space != nil {
t.Errorf("GetSpaceByToken() expected nil, got %v", space)
}
if !tt.wantNil && space == nil {
t.Errorf("GetSpaceByToken() expected space, got nil")
}
})
}
}
func TestGetSpaceBySubdomain(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
created, err := db.CreateSpace(ctx, "mytestspace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
subdomain string
wantNil bool
}{
{
name: "existing subdomain",
subdomain: "mytestspace",
wantNil: false,
},
{
name: "non-existent subdomain",
subdomain: "nonexistent",
wantNil: true,
},
{
name: "case sensitivity test",
subdomain: "MyTestSpace",
wantNil: true, // SQLite is case-sensitive by default
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
space, err := db.GetSpaceBySubdomain(ctx, tt.subdomain)
if err != nil {
t.Errorf("GetSpaceBySubdomain() unexpected error: %v", err)
return
}
if tt.wantNil && space != nil {
t.Errorf("GetSpaceBySubdomain() expected nil, got %v", space)
}
if !tt.wantNil && space == nil {
t.Errorf("GetSpaceBySubdomain() expected space, got nil")
}
if !tt.wantNil && space.Token != created.Token {
t.Errorf("GetSpaceBySubdomain() token mismatch")
}
})
}
}
func TestUpdateSpaceIP(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
created, err := db.CreateSpace(ctx, "updatespace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
token string
ip string
wantErr bool
}{
{
name: "update valid space",
token: created.Token,
ip: "192.168.1.100",
wantErr: false,
},
{
name: "update with different IP",
token: created.Token,
ip: "10.0.0.50",
wantErr: false,
},
{
name: "update non-existent space",
token: "invalid-token",
ip: "192.168.1.1",
wantErr: false, // SQL UPDATE with no match doesn't error
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := db.UpdateSpaceIP(ctx, tt.token, tt.ip)
if tt.wantErr && err == nil {
t.Errorf("UpdateSpaceIP() expected error but got none")
}
if !tt.wantErr && err != nil {
t.Errorf("UpdateSpaceIP() unexpected error: %v", err)
}
// Verify the update for valid token
if !tt.wantErr && tt.token == created.Token {
space, err := db.GetSpaceByToken(ctx, tt.token)
if err != nil {
t.Errorf("Failed to get space after update: %v", err)
return
}
if space.LastIP != tt.ip {
t.Errorf("UpdateSpaceIP() IP = %v, want %v", space.LastIP, tt.ip)
}
}
})
}
}
func TestSubdomainExists(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a test space
_, err := db.CreateSpace(ctx, "existingspace")
if err != nil {
t.Fatalf("Failed to create test space: %v", err)
}
tests := []struct {
name string
subdomain string
want bool
}{
{
name: "existing subdomain",
subdomain: "existingspace",
want: true,
},
{
name: "non-existent subdomain",
subdomain: "newspace",
want: false,
},
{
name: "empty subdomain",
subdomain: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exists, err := db.SubdomainExists(ctx, tt.subdomain)
if err != nil {
t.Errorf("SubdomainExists() unexpected error: %v", err)
return
}
if exists != tt.want {
t.Errorf("SubdomainExists() = %v, want %v", exists, tt.want)
}
})
}
}
func TestTokenGeneration(t *testing.T) {
tokens := make(map[string]bool)
// Generate 100 tokens and ensure they're all unique
for i := 0; i < 100; i++ {
token, err := generateToken()
if err != nil {
t.Fatalf("generateToken() error: %v", err)
}
if token == "" {
t.Errorf("generateToken() returned empty string")
}
if len(token) < 16 {
t.Errorf("generateToken() returned short token: %d chars", len(token))
}
if tokens[token] {
t.Errorf("generateToken() returned duplicate token: %s", token)
}
tokens[token] = true
}
}
func TestSpaceModel(t *testing.T) {
space := &models.Space{
Token: "test-token",
Subdomain: "myspace",
LastIP: "192.168.1.1",
}
fqdn := space.GetFQDN("space.dws.rip")
if fqdn != "myspace.space.dws.rip" {
t.Errorf("GetFQDN() = %v, want %v", fqdn, "myspace.space.dws.rip")
}
}

View File

@@ -0,0 +1,243 @@
package handlers
import (
"testing"
)
func TestCustomFilter_IsBlocked(t *testing.T) {
cf := NewCustomFilter()
tests := []struct {
name string
subdomain string
want bool
}{
{
name: "exact match - dws",
subdomain: "dws",
want: true,
},
{
name: "exact match - dubey",
subdomain: "dubey",
want: true,
},
{
name: "exact match - tanishq",
subdomain: "tanishq",
want: true,
},
{
name: "exact match - tdubey",
subdomain: "tdubey",
want: true,
},
{
name: "with hyphens - dubey-web",
subdomain: "dubey-web",
want: true,
},
{
name: "with hyphens - tanishq-dubey",
subdomain: "tanishq-dubey",
want: true,
},
{
name: "leet speak - dub3y",
subdomain: "dub3y",
want: true,
},
{
name: "leet speak - t4nishq",
subdomain: "t4nishq",
want: true,
},
{
name: "leet speak - dw5",
subdomain: "dw5",
want: true,
},
{
name: "combined term - dubeydns",
subdomain: "dubeydns",
want: true,
},
{
name: "combined term - dws-ddns",
subdomain: "dws-ddns",
want: true,
},
{
name: "safe subdomain - myhome",
subdomain: "myhome",
want: false,
},
{
name: "safe subdomain - office",
subdomain: "office",
want: false,
},
{
name: "safe subdomain - server01",
subdomain: "server01",
want: false,
},
{
name: "case insensitive - DWS",
subdomain: "DWS",
want: true,
},
{
name: "case insensitive - Tanishq",
subdomain: "Tanishq",
want: true,
},
{
name: "mixed case - DuBeY",
subdomain: "DuBeY",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cf.IsBlocked(tt.subdomain)
if got != tt.want {
t.Errorf("IsBlocked(%q) = %v, want %v", tt.subdomain, got, tt.want)
}
})
}
}
func TestCustomFilter_normalize(t *testing.T) {
cf := NewCustomFilter()
tests := []struct {
name string
text string
want string
}{
{
name: "lowercase conversion",
text: "DuBeY",
want: "dubey",
},
{
name: "remove hyphens",
text: "dubey-web",
want: "dubeyweb",
},
{
name: "remove underscores",
text: "dubey_web",
want: "dubeyweb",
},
{
name: "remove dots",
text: "dubey.web",
want: "dubeyweb",
},
{
name: "remove spaces",
text: "dubey web",
want: "dubeyweb",
},
{
name: "leet speak conversion",
text: "dub3y",
want: "dubey",
},
{
name: "leet speak conversion - t4nishq",
text: "t4nishq",
want: "tanishq",
},
{
name: "leet speak conversion - dw5",
text: "dw5",
want: "dws",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cf.normalize(tt.text)
if got != tt.want {
t.Errorf("normalize(%q) = %q, want %q", tt.text, got, tt.want)
}
})
}
}
func TestIsValidSubdomain(t *testing.T) {
tests := []struct {
name string
subdomain string
want bool
}{
{
name: "valid - simple",
subdomain: "myhome",
want: true,
},
{
name: "valid - with hyphen",
subdomain: "my-home",
want: true,
},
{
name: "valid - with numbers",
subdomain: "home123",
want: true,
},
{
name: "valid - minimum length",
subdomain: "abc",
want: true,
},
{
name: "invalid - too short",
subdomain: "ab",
want: false,
},
{
name: "invalid - too long",
subdomain: "thisisaverylongsubdomainthatexceedsthesixtythreecharacterlimitforsubdomains",
want: false,
},
{
name: "invalid - starts with hyphen",
subdomain: "-myhome",
want: false,
},
{
name: "invalid - ends with hyphen",
subdomain: "myhome-",
want: false,
},
{
name: "invalid - contains special chars",
subdomain: "my_home",
want: false,
},
{
name: "invalid - contains dot",
subdomain: "my.home",
want: false,
},
{
name: "invalid - empty",
subdomain: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isValidSubdomain(tt.subdomain)
if got != tt.want {
t.Errorf("isValidSubdomain(%q) = %v, want %v", tt.subdomain, got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,45 @@
package handlers
import (
"regexp"
"strings"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
func init() {
// Register custom validators
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("alphanumdash", alphanumdashValidator)
}
}
// alphanumdashValidator validates that a string contains only alphanumeric characters and hyphens
var alphanumdashValidator validator.Func = func(fl validator.FieldLevel) bool {
value := fl.Field().String()
// Allow alphanumeric and hyphens only
match := regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(value)
return match
}
// IsValidSubdomainFormat checks if a subdomain string is valid (exported for use in validation)
func IsValidSubdomainFormat(subdomain string) bool {
// Must be at least 3 characters
if len(subdomain) < 3 {
return false
}
// Must not exceed 63 characters
if len(subdomain) > 63 {
return false
}
// Must not start or end with hyphen
if strings.HasPrefix(subdomain, "-") || strings.HasSuffix(subdomain, "-") {
return false
}
// Must contain only alphanumeric and hyphens
return regexp.MustCompile(`^[a-zA-Z0-9-]+$`).MatchString(subdomain)
}

View File

@@ -0,0 +1,238 @@
package testutil
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sync"
)
// MockTechnitiumServer simulates the Technitium DNS API for testing
type MockTechnitiumServer struct {
Server *httptest.Server
Records map[string]MockDNSRecord
mu sync.RWMutex
Username string
Password string
Token string
}
// MockDNSRecord represents a DNS record stored in the mock server
type MockDNSRecord struct {
Domain string `json:"domain"`
Type string `json:"type"`
IPAddress string `json:"ipAddress"`
TTL int `json:"ttl"`
}
// NewMockTechnitiumServer creates a new mock Technitium server
func NewMockTechnitiumServer() *MockTechnitiumServer {
mock := &MockTechnitiumServer{
Records: make(map[string]MockDNSRecord),
Username: "admin",
Password: "test-password",
Token: "test-api-token",
}
mux := http.NewServeMux()
mux.HandleFunc("/api/dns/records/add", mock.handleAddRecord)
mux.HandleFunc("/api/dns/records/delete", mock.handleDeleteRecord)
mux.HandleFunc("/api/dns/records/get", mock.handleGetRecords)
// Health check endpoint
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Technitium DNS Server Mock"))
})
mock.Server = httptest.NewServer(mux)
return mock
}
// Close shuts down the mock server
func (m *MockTechnitiumServer) Close() {
m.Server.Close()
}
// URL returns the base URL of the mock server
func (m *MockTechnitiumServer) URL() string {
return m.Server.URL
}
// GetRecords returns all stored DNS records (for testing assertions)
func (m *MockTechnitiumServer) GetRecords() map[string]MockDNSRecord {
m.mu.RLock()
defer m.mu.RUnlock()
// Return a copy to avoid race conditions
records := make(map[string]MockDNSRecord)
for k, v := range m.Records {
records[k] = v
}
return records
}
// GetRecordCount returns the number of stored records
func (m *MockTechnitiumServer) GetRecordCount() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.Records)
}
// ClearRecords removes all stored records
func (m *MockTechnitiumServer) ClearRecords() {
m.mu.Lock()
defer m.mu.Unlock()
m.Records = make(map[string]MockDNSRecord)
}
func (m *MockTechnitiumServer) authenticate(r *http.Request) bool {
// Check for API token in header
authHeader := r.Header.Get("Authorization")
if authHeader == "Basic "+m.Token {
return true
}
// Check for username/password in basic auth
user, pass, ok := r.BasicAuth()
if ok && user == m.Username && pass == m.Password {
return true
}
return false
}
func (m *MockTechnitiumServer) handleAddRecord(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !m.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "error",
"error": map[string]string{
"code": "Unauthorized",
"message": "Invalid credentials",
},
})
return
}
// Parse form data
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
domain := r.FormValue("domain")
recordType := r.FormValue("type")
ipAddress := r.FormValue("ipAddress")
if domain == "" || recordType == "" {
http.Error(w, "Missing required fields", http.StatusBadRequest)
return
}
// Store the record
m.mu.Lock()
key := fmt.Sprintf("%s:%s", domain, recordType)
m.Records[key] = MockDNSRecord{
Domain: domain,
Type: recordType,
IPAddress: ipAddress,
TTL: 300,
}
m.mu.Unlock()
// Return success response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"response": map[string]interface{}{
"domain": domain,
"type": recordType,
},
})
}
func (m *MockTechnitiumServer) handleDeleteRecord(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !m.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "error",
"error": map[string]string{
"code": "Unauthorized",
"message": "Invalid credentials",
},
})
return
}
// Parse form data
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
domain := r.FormValue("domain")
recordType := r.FormValue("type")
// Delete the record
m.mu.Lock()
key := fmt.Sprintf("%s:%s", domain, recordType)
delete(m.Records, key)
m.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
})
}
func (m *MockTechnitiumServer) handleGetRecords(w http.ResponseWriter, r *http.Request) {
if !m.authenticate(r) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "error",
"error": map[string]string{
"code": "Unauthorized",
"message": "Invalid credentials",
},
})
return
}
domain := r.URL.Query().Get("domain")
m.mu.RLock()
var records []MockDNSRecord
for _, record := range m.Records {
if domain == "" || record.Domain == domain {
records = append(records, record)
}
}
m.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"response": map[string]interface{}{
"domain": domain,
"records": records,
},
})
}