mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 17:16:59 +02:00
913 lines
24 KiB
Go
913 lines
24 KiB
Go
/*
|
|
Copyright 2025 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package pihole
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sigs.k8s.io/external-dns/endpoint"
|
|
)
|
|
|
|
func TestIsValidIPv4(t *testing.T) {
|
|
tests := []struct {
|
|
ip string
|
|
expected bool
|
|
}{
|
|
{"192.168.1.1", true},
|
|
{"255.255.255.255", true},
|
|
{"0.0.0.0", true},
|
|
{"", false},
|
|
{"256.256.256.256", false},
|
|
{"192.168.0.1/22", false},
|
|
{"192.168.1", false},
|
|
{"abc.def.ghi.jkl", false},
|
|
{"::ffff:192.168.20.3", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.ip, func(t *testing.T) {
|
|
if got := isValidIPv4(test.ip); got != test.expected {
|
|
t.Errorf("isValidIPv4(%s) = %v; want %v", test.ip, got, test.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidIPv6(t *testing.T) {
|
|
tests := []struct {
|
|
ip string
|
|
expected bool
|
|
}{
|
|
{"2001:0db8:85a3:0000:0000:8a2e:0370:7334", true},
|
|
{"2001:db8:85a3::8a2e:370:7334", true},
|
|
//IPv6 dual, the format is y:y:y:y:y:y:x.x.x.x.
|
|
{"::ffff:192.168.20.3", true},
|
|
{"::1", true},
|
|
{"::", true},
|
|
{"2001:db8::", true},
|
|
{"", false},
|
|
{":", false},
|
|
{"::ffff:", false},
|
|
{"192.168.20.3", false},
|
|
{"2001:db8:85a3:0:0:8a2e:370:7334:1234", false},
|
|
{"2001:db8:85a3::8a2e:370g:7334", false},
|
|
{"2001:db8:85a3::8a2e:370:7334::", false},
|
|
{"2001:db8:85a3::8a2e:370:7334::1", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.ip, func(t *testing.T) {
|
|
if got := isValidIPv6(test.ip); got != test.expected {
|
|
t.Errorf("isValidIPv6(%s) = %v; want %v", test.ip, got, test.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func newTestServerV6(t *testing.T, hdlr http.HandlerFunc) *httptest.Server {
|
|
t.Helper()
|
|
svr := httptest.NewServer(hdlr)
|
|
return svr
|
|
}
|
|
|
|
func TestNewPiholeClientV6(t *testing.T) {
|
|
// Test correct error on no server provided
|
|
_, err := newPiholeClientV6(PiholeConfig{APIVersion: "6"})
|
|
if err == nil {
|
|
t.Error("Expected error from config with no server")
|
|
} else if err != ErrNoPiholeServer {
|
|
t.Error("Expected ErrNoPiholeServer, got", err)
|
|
}
|
|
|
|
// Test new client with no password. Should create the client cleanly.
|
|
cl, err := newPiholeClientV6(PiholeConfig{
|
|
Server: "test",
|
|
APIVersion: "6",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := cl.(*piholeClientV6); !ok {
|
|
t.Error("Did not create a new pihole client")
|
|
}
|
|
|
|
// Create a test server
|
|
srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/auth" && r.Method == "POST" {
|
|
var requestData map[string]string
|
|
json.NewDecoder(r.Body).Decode(&requestData)
|
|
defer r.Body.Close()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if requestData["password"] != "correct" {
|
|
// Return unsuccessful authentication response
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
w.Write([]byte(`{
|
|
"session": {
|
|
"valid": false,
|
|
"totp": false,
|
|
"sid": null,
|
|
"validity": -1,
|
|
"message": "password incorrect"
|
|
},
|
|
"took": 0.2
|
|
}`))
|
|
return
|
|
}
|
|
|
|
// Return successful authentication response
|
|
w.Write([]byte(`{
|
|
"session": {
|
|
"valid": true,
|
|
"totp": false,
|
|
"sid": "supersecret",
|
|
"csrf": "csrfvalue",
|
|
"validity": 1800,
|
|
"message": "password correct"
|
|
},
|
|
"took": 0.18
|
|
}`))
|
|
} else {
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
defer srvr.Close()
|
|
|
|
// Test invalid password
|
|
_, err = newPiholeClientV6(
|
|
PiholeConfig{Server: srvr.URL, APIVersion: "6", Password: "wrong"},
|
|
)
|
|
if err == nil {
|
|
t.Error("Expected error for creating client with invalid password")
|
|
}
|
|
|
|
// Test correct password
|
|
cl, err = newPiholeClientV6(
|
|
PiholeConfig{Server: srvr.URL, APIVersion: "6", Password: "correct"},
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cl.(*piholeClientV6).token != "supersecret" {
|
|
t.Error("Parsed invalid token from login response:", cl.(*piholeClient).token)
|
|
}
|
|
}
|
|
|
|
func TestListRecordsV6(t *testing.T) {
|
|
// Create a test server
|
|
srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/config/dns/hosts" && r.Method == "GET" {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Return A records
|
|
w.Write([]byte(`{
|
|
"config": {
|
|
"dns": {
|
|
"hosts": [
|
|
"192.168.178.33 service1.example.com",
|
|
"192.168.178.34 service2.example.com",
|
|
"192.168.178.34 service3.example.com",
|
|
"fc00::1:192:168:1:1 service4.example.com",
|
|
"fc00::1:192:168:1:2 service5.example.com",
|
|
"fc00::1:192:168:1:3 service6.example.com",
|
|
"::ffff:192.168.20.3 service7.example.com",
|
|
"192.168.20.3 service7.example.com"
|
|
]
|
|
}
|
|
},
|
|
"took": 5
|
|
}`))
|
|
} else if r.URL.Path == "/api/config/dns/cnameRecords" && r.Method == "GET" {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Return A records
|
|
w.Write([]byte(`{
|
|
"config": {
|
|
"dns": {
|
|
"cnameRecords": [
|
|
"source1.example.com,target1.domain.com,1000",
|
|
"source2.example.com,target2.domain.com,50",
|
|
"source3.example.com,target3.domain.com"
|
|
]
|
|
}
|
|
},
|
|
"took": 5
|
|
}`))
|
|
} else {
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
defer srvr.Close()
|
|
|
|
// Create a client
|
|
cfg := PiholeConfig{
|
|
Server: srvr.URL,
|
|
APIVersion: "6",
|
|
}
|
|
cl, err := newPiholeClientV6(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Ensure A records were parsed correctly
|
|
expected := [][]string{
|
|
{"service1.example.com", "192.168.178.33"},
|
|
{"service2.example.com", "192.168.178.34"},
|
|
{"service3.example.com", "192.168.178.34"},
|
|
{"service7.example.com", "192.168.20.3"},
|
|
}
|
|
// Test retrieve A records unfiltered
|
|
arecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeA)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(arecs) != len(expected) {
|
|
t.Fatalf("Expected %d A records returned, got: %d", len(expected), len(arecs))
|
|
}
|
|
|
|
for idx, rec := range arecs {
|
|
if rec.DNSName != expected[idx][0] {
|
|
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
|
|
}
|
|
if rec.Targets[0] != expected[idx][1] {
|
|
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
|
|
}
|
|
}
|
|
|
|
// Ensure AAAA records were parsed correctly
|
|
expected = [][]string{
|
|
{"service4.example.com", "fc00::1:192:168:1:1"},
|
|
{"service5.example.com", "fc00::1:192:168:1:2"},
|
|
{"service6.example.com", "fc00::1:192:168:1:3"},
|
|
{"service7.example.com", "::ffff:192.168.20.3"},
|
|
}
|
|
// Test retrieve AAAA records unfiltered
|
|
arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(arecs) != len(expected) {
|
|
t.Fatalf("Expected %d AAAA records returned, got: %d", len(expected), len(arecs))
|
|
}
|
|
|
|
for idx, rec := range arecs {
|
|
if rec.DNSName != expected[idx][0] {
|
|
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
|
|
}
|
|
if rec.Targets[0] != expected[idx][1] {
|
|
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
|
|
}
|
|
}
|
|
|
|
// Ensure CNAME records were parsed correctly
|
|
expected = [][]string{
|
|
{"source1.example.com", "target1.domain.com", "1000"},
|
|
{"source2.example.com", "target2.domain.com", "50"},
|
|
{"source3.example.com", "target3.domain.com"},
|
|
}
|
|
|
|
// Test retrieve CNAME records unfiltered
|
|
cnamerecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeCNAME)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(cnamerecs) != len(expected) {
|
|
t.Fatalf("Expected %d CAME records returned, got: %d", len(expected), len(cnamerecs))
|
|
}
|
|
|
|
for idx, rec := range cnamerecs {
|
|
if rec.DNSName != expected[idx][0] {
|
|
t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0])
|
|
}
|
|
if rec.Targets[0] != expected[idx][1] {
|
|
t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1])
|
|
}
|
|
if len(expected[idx]) == 3 {
|
|
expectedTTL, _ := strconv.ParseInt(expected[idx][2], 10, 64)
|
|
if int64(rec.RecordTTL) != expectedTTL {
|
|
t.Error("Got invalid TTL:", rec.RecordTTL, "expected:", expected[idx][2])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: filtered tests are not needed since A/AAAA records are tested filtered already
|
|
// and cnameRecords have their own element
|
|
|
|
// unsupported type
|
|
_, err = cl.listRecords(context.Background(), endpoint.RecordTypeNAPTR)
|
|
if err == nil || err.Error() != fmt.Sprintf("unsupported record type: %s", endpoint.RecordTypeNAPTR) {
|
|
t.Fatal("Expected error for using unsupported record type")
|
|
}
|
|
}
|
|
|
|
func TestErrorsV6(t *testing.T) {
|
|
//Error test cases
|
|
|
|
// Create a client
|
|
cfgErrURL := PiholeConfig{
|
|
Server: "not an url",
|
|
APIVersion: "6",
|
|
}
|
|
clErrURL, _ := newPiholeClientV6(cfgErrURL)
|
|
|
|
_, err := clErrURL.listRecords(context.Background(), endpoint.RecordTypeCNAME)
|
|
if err == nil {
|
|
t.Fatal("Expected error for using invalid URL")
|
|
}
|
|
_, err = clErrURL.listRecords(nil, endpoint.RecordTypeCNAME)
|
|
if err == nil {
|
|
t.Fatal("Expected error for nil context")
|
|
}
|
|
// Unmarshalling error
|
|
srvrErrJson := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Return A records
|
|
w.Write([]byte(`I am not JSON`))
|
|
})
|
|
defer srvrErrJson.Close()
|
|
// Create a client
|
|
cfgErr := PiholeConfig{
|
|
Server: srvrErrJson.URL,
|
|
APIVersion: "6",
|
|
}
|
|
clErr, _ := newPiholeClientV6(cfgErr)
|
|
|
|
resp, err := clErr.listRecords(context.Background(), endpoint.RecordTypeA)
|
|
if err == nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !strings.HasPrefix(err.Error(), "failed to unmarshal error response:") {
|
|
t.Fatal("Expected unmarshalling error, got:", err)
|
|
}
|
|
|
|
// bad record format return by server
|
|
srvrErr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/config/dns/hosts" && r.Method == "GET" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Return A records
|
|
w.Write([]byte(`{
|
|
"config": {
|
|
"dns": {
|
|
"hosts": [
|
|
"192.168.178.33"
|
|
]
|
|
}
|
|
},
|
|
"took": 5
|
|
}`))
|
|
} else if r.URL.Path == "/api/config/dns/cnameRecords" && r.Method == "GET" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Return A records
|
|
w.Write([]byte(`{
|
|
"config": {
|
|
"dns": {
|
|
"cnameRecords": [
|
|
"source1.example.com,target1.domain.com,100",
|
|
"source2.example.com,target2.domain.com,not_an_integer"
|
|
]
|
|
}
|
|
},
|
|
"took": 5
|
|
}`))
|
|
} else {
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
defer srvrErr.Close()
|
|
|
|
// Create a client
|
|
cfgErr = PiholeConfig{
|
|
Server: srvrErr.URL,
|
|
APIVersion: "6",
|
|
}
|
|
clErr, _ = newPiholeClientV6(cfgErr)
|
|
|
|
resp, err = clErr.listRecords(context.Background(), endpoint.RecordTypeA)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(resp) != 0 {
|
|
t.Fatal("Expected no records returned, got:", len(resp))
|
|
}
|
|
resp, err = clErr.listRecords(context.Background(), endpoint.RecordTypeCNAME)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(resp) != 2 {
|
|
t.Fatal("Expected one records returned, got:", len(resp))
|
|
}
|
|
if resp[1].RecordTTL != 0 {
|
|
t.Fatal("Expected no TTL returned, got:", resp[0].RecordTTL)
|
|
}
|
|
|
|
}
|
|
|
|
func TestTokenValidity(t *testing.T) {
|
|
srvok := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/auth" && r.Method == "GET" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"session": {
|
|
"valid": true,
|
|
"totp": false,
|
|
"sid": "supersecret",
|
|
"csrf": "csrfvalue",
|
|
"validity": 1800,
|
|
"message": "password correct"
|
|
},
|
|
"took": 0.17
|
|
}`))
|
|
}
|
|
})
|
|
// Create a client
|
|
cfgOK := PiholeConfig{
|
|
Server: srvok.URL,
|
|
APIVersion: "6",
|
|
}
|
|
clOK, err := newPiholeClientV6(cfgOK)
|
|
clOK.(*piholeClientV6).token = "valid"
|
|
validity, err := clOK.(*piholeClientV6).checkTokenValidity(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !validity {
|
|
t.Fatal("Should be valid")
|
|
}
|
|
|
|
// Create a test server
|
|
srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.URL.Path == "/api/auth" && r.Method == "GET" {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Return bad content
|
|
w.Write([]byte(`Not a JSON`))
|
|
}
|
|
})
|
|
defer srvr.Close()
|
|
//
|
|
// Create a client
|
|
cfg := PiholeConfig{
|
|
Server: srvr.URL,
|
|
APIVersion: "6",
|
|
}
|
|
cl, err := newPiholeClientV6(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
validity, err = cl.(*piholeClientV6).checkTokenValidity(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if validity {
|
|
t.Fatal("Should be invalid : no token")
|
|
}
|
|
// Test token validity
|
|
cl.(*piholeClientV6).token = "valid"
|
|
|
|
validity, err = cl.(*piholeClientV6).checkTokenValidity(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if validity {
|
|
t.Fatal("Should be invalid : nil context")
|
|
}
|
|
|
|
validity, err = cl.(*piholeClientV6).checkTokenValidity(context.Background())
|
|
if err == nil {
|
|
t.Fatal("Should be invalid : failed to unmarshal error")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "failed to unmarshal error response") {
|
|
t.Fatal("Expected unmarshalling error, got:", err)
|
|
}
|
|
if validity {
|
|
t.Fatal("Should be invalid : unmarshalling error")
|
|
}
|
|
}
|
|
|
|
func TestDo(t *testing.T) {
|
|
|
|
srvDo := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/auth/ok" && r.Method == "GET" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"session": {
|
|
"valid": true,
|
|
"totp": false,
|
|
"sid": "supersecret",
|
|
"csrf": "csrfvalue",
|
|
"validity": 1800,
|
|
"message": "password correct"
|
|
},
|
|
"took": 0.16
|
|
}`))
|
|
} else if r.URL.Path == "/api/auth" && r.Method == "POST" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"session": {
|
|
"valid": false,
|
|
"totp": false,
|
|
"sid": "",
|
|
"csrf": "csrfvalue",
|
|
"validity": 1800,
|
|
"message": "password correct"
|
|
},
|
|
"took": 0.15
|
|
}`))
|
|
} else if r.URL.Path == "/api/auth" && r.Method == "GET" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"error": {
|
|
"key": "401",
|
|
"message": "Expired token",
|
|
"hint": "Expired token"
|
|
},
|
|
"took": 0.14
|
|
}`))
|
|
} else if r.URL.Path == "/api/auth/418" && r.Method == "GET" {
|
|
w.WriteHeader(http.StatusTeapot)
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"error": {
|
|
"key": "418",
|
|
"message": "I'm a teapot",
|
|
"hint": "It is a teapot"
|
|
},
|
|
"took": 0.13
|
|
}`))
|
|
} else if r.URL.Path == "/api/auth/nojson" && r.Method == "GET" {
|
|
// Return bad content
|
|
w.WriteHeader(http.StatusTeapot)
|
|
w.Write([]byte(`Not a JSON`))
|
|
} else if r.URL.Path == "/api/auth/401" && r.Method == "GET" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"error": {
|
|
"key": "401",
|
|
"message": "Expired token",
|
|
"hint": "Expired token"
|
|
},
|
|
"took": 0.10
|
|
}`))
|
|
}
|
|
})
|
|
defer srvDo.Close()
|
|
|
|
// Create a client
|
|
cfg := PiholeConfig{
|
|
Server: srvDo.URL,
|
|
APIVersion: "6",
|
|
}
|
|
cl, err := newPiholeClientV6(cfg)
|
|
cl.(*piholeClientV6).token = "valid"
|
|
|
|
rq, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/ok", nil)
|
|
resp, err := cl.(*piholeClientV6).do(rq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(resp) == 0 {
|
|
t.Fatal("Should have a response")
|
|
}
|
|
// Test not handled error code
|
|
rq, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/418", nil)
|
|
resp, err = cl.(*piholeClientV6).do(rq)
|
|
if resp != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("Should have an error")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "received 418 status code from request") {
|
|
t.Fatal("Expected error for unexpected status code, got:", err)
|
|
}
|
|
// Test error on non JSON response
|
|
rq, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/nojson", nil)
|
|
resp, err = cl.(*piholeClientV6).do(rq)
|
|
if resp != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("Should have an error")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "failed to unmarshal error response") {
|
|
t.Fatal("Expected error for unmarshal", err)
|
|
}
|
|
// Test Unauthorized retry failed
|
|
rq, _ = http.NewRequestWithContext(context.Background(), http.MethodGet, srvDo.URL+"/api/auth/401", nil)
|
|
resp, err = cl.(*piholeClientV6).do(rq)
|
|
if resp != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err == nil {
|
|
t.Fatal("Should have an error")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "max tries reached for token renewal") {
|
|
t.Fatal("Expected error for max tries reached", err)
|
|
}
|
|
}
|
|
|
|
func TestDoRetryOne(t *testing.T) {
|
|
nbCall := 0
|
|
srvRetry := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/auth" && r.Method == "GET" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"session": {
|
|
"valid": true,
|
|
"totp": false,
|
|
"sid": "123465468",
|
|
"csrf": "csrfvalue",
|
|
"validity": 1800,
|
|
"message": "password correct"
|
|
},
|
|
"took": 0.24
|
|
}`))
|
|
} else if r.URL.Path == "/api/auth/401" && r.Method == "GET" {
|
|
if nbCall == 0 {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
// Return bad content
|
|
w.Write([]byte(`{
|
|
"error": {
|
|
"key": "401",
|
|
"message": "Expired token",
|
|
"hint": "Expired token"
|
|
},
|
|
"took": 0.25
|
|
}`))
|
|
} else {
|
|
w.WriteHeader(http.StatusOK)
|
|
// Return bad content
|
|
w.Write([]byte(`Success`))
|
|
}
|
|
nbCall += 1
|
|
}
|
|
})
|
|
defer srvRetry.Close()
|
|
// Create a client
|
|
cfgRetryOK := PiholeConfig{
|
|
Server: srvRetry.URL,
|
|
APIVersion: "6",
|
|
}
|
|
clRetryOK, err := newPiholeClientV6(cfgRetryOK)
|
|
clRetryOK.(*piholeClientV6).token = "valid"
|
|
// Test Unauthorized refresh OK
|
|
rq, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srvRetry.URL+"/api/auth/401", nil)
|
|
resp, err := clRetryOK.(*piholeClientV6).do(rq)
|
|
if err != nil {
|
|
t.Fatal("Should succeed", err)
|
|
}
|
|
if string(resp) != "Success" {
|
|
t.Fatal("Should have a response")
|
|
}
|
|
|
|
}
|
|
|
|
func TestCreateRecordV6(t *testing.T) {
|
|
var ep *endpoint.Endpoint
|
|
srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "PUT" && (r.URL.Path == "/api/config/dns/hosts/192.168.1.1 test.example.com" ||
|
|
r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:1 test.example.com" ||
|
|
r.URL.Path == "/api/config/dns/cnameRecords/source1.example.com,target1.domain.com" ||
|
|
r.URL.Path == "/api/config/dns/cnameRecords/source2.example.com,target2.domain.com,500") {
|
|
|
|
// Return A records
|
|
w.WriteHeader(http.StatusCreated)
|
|
} else {
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
defer srvr.Close()
|
|
|
|
// Create a client
|
|
cfg := PiholeConfig{
|
|
Server: srvr.URL,
|
|
APIVersion: "6",
|
|
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
|
}
|
|
cl, err := newPiholeClientV6(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test create A record
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "test.example.com",
|
|
Targets: []string{"192.168.1.1"},
|
|
RecordType: endpoint.RecordTypeA,
|
|
}
|
|
if err := cl.createRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test create AAAA record
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "test.example.com",
|
|
Targets: []string{"fc00::1:192:168:1:1"},
|
|
RecordType: endpoint.RecordTypeAAAA,
|
|
}
|
|
if err := cl.createRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test create CNAME record
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "source1.example.com",
|
|
Targets: []string{"target1.domain.com"},
|
|
RecordType: endpoint.RecordTypeCNAME,
|
|
}
|
|
if err := cl.createRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test create CNAME record with TTL
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "source2.example.com",
|
|
Targets: []string{"target2.domain.com"},
|
|
RecordTTL: endpoint.TTL(500),
|
|
RecordType: endpoint.RecordTypeCNAME,
|
|
}
|
|
if err := cl.createRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test create a wildcard record and ensure it fails
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "*.example.com",
|
|
Targets: []string{"192.168.1.1"},
|
|
RecordType: endpoint.RecordTypeA,
|
|
}
|
|
if err := cl.createRecord(context.Background(), ep); err == nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Skip not matching domain
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "foo.bar.com",
|
|
Targets: []string{"192.168.1.1"},
|
|
RecordType: endpoint.RecordTypeA,
|
|
}
|
|
err = cl.createRecord(context.Background(), ep)
|
|
if err != nil {
|
|
t.Fatal("Should not return error on non filtered domain")
|
|
}
|
|
|
|
// Not supported type
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "test.example.com",
|
|
Targets: []string{"192.168.1.1"},
|
|
RecordType: "not a type",
|
|
}
|
|
err = cl.createRecord(context.Background(), ep)
|
|
if err != nil {
|
|
t.Fatal("Should not return error on unsupported type")
|
|
}
|
|
|
|
// Create a client
|
|
cfgDr := PiholeConfig{
|
|
Server: srvr.URL,
|
|
APIVersion: "6",
|
|
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
|
DryRun: true,
|
|
}
|
|
clDr, err := newPiholeClientV6(cfgDr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Skip Dry Run
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "test.example.com",
|
|
Targets: []string{"192.168.1.1"},
|
|
RecordType: endpoint.RecordTypeA,
|
|
}
|
|
err = clDr.createRecord(context.Background(), ep)
|
|
if err != nil {
|
|
t.Fatal("Should not return error on dry run")
|
|
}
|
|
// skip missing targets
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "test.example.com",
|
|
Targets: []string{},
|
|
RecordType: endpoint.RecordTypeA,
|
|
}
|
|
err = clDr.createRecord(context.Background(), ep)
|
|
if err != nil {
|
|
t.Fatal("Should not return error on missing targets")
|
|
}
|
|
}
|
|
|
|
func TestDeleteRecordV6(t *testing.T) {
|
|
var ep *endpoint.Endpoint
|
|
srvr := newTestServerV6(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "DELETE" && (r.URL.Path == "/api/config/dns/hosts/192.168.1.1 test.example.com" ||
|
|
r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:1 test.example.com" ||
|
|
r.URL.Path == "/api/config/dns/cnameRecords/source1.example.com,target1.domain.com" ||
|
|
r.URL.Path == "/api/config/dns/cnameRecords/source2.example.com,target2.domain.com,500") {
|
|
|
|
// Return A records
|
|
w.WriteHeader(http.StatusNoContent)
|
|
} else {
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
defer srvr.Close()
|
|
|
|
// Create a client
|
|
cfg := PiholeConfig{
|
|
Server: srvr.URL,
|
|
APIVersion: "6",
|
|
}
|
|
cl, err := newPiholeClientV6(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test delete A record
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "test.example.com",
|
|
Targets: []string{"192.168.1.1"},
|
|
RecordType: endpoint.RecordTypeA,
|
|
}
|
|
if err := cl.deleteRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test delete AAAA record
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "test.example.com",
|
|
Targets: []string{"fc00::1:192:168:1:1"},
|
|
RecordType: endpoint.RecordTypeAAAA,
|
|
}
|
|
if err := cl.deleteRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test delete CNAME record
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "source1.example.com",
|
|
Targets: []string{"target1.domain.com"},
|
|
RecordType: endpoint.RecordTypeCNAME,
|
|
}
|
|
if err := cl.deleteRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Test delete CNAME record with TTL
|
|
ep = &endpoint.Endpoint{
|
|
DNSName: "source2.example.com",
|
|
Targets: []string{"target2.domain.com"},
|
|
RecordTTL: endpoint.TTL(500),
|
|
RecordType: endpoint.RecordTypeCNAME,
|
|
}
|
|
if err := cl.deleteRecord(context.Background(), ep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|