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, }, }) }