lego/providers/dns/zoneee/zoneee_test.go
2024-03-20 20:30:35 +01:00

420 lines
10 KiB
Go

package zoneee
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"path"
"testing"
"time"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/go-acme/lego/v4/providers/dns/zoneee/internal"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(EnvEndpoint, EnvAPIUser, EnvAPIKey).
WithLiveTestRequirements(EnvAPIUser, EnvAPIKey).
WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvAPIUser: "123",
EnvAPIKey: "456",
},
},
{
desc: "missing credentials",
envVars: map[string]string{
EnvAPIUser: "",
EnvAPIKey: "",
},
expected: "zoneee: some credentials information are missing: ZONEEE_API_USER,ZONEEE_API_KEY",
},
{
desc: "missing username",
envVars: map[string]string{
EnvAPIUser: "",
EnvAPIKey: "456",
},
expected: "zoneee: some credentials information are missing: ZONEEE_API_USER",
},
{
desc: "missing API key",
envVars: map[string]string{
EnvAPIUser: "123",
EnvAPIKey: "",
},
expected: "zoneee: some credentials information are missing: ZONEEE_API_KEY",
},
{
desc: "invalid URL",
envVars: map[string]string{
EnvAPIUser: "123",
EnvAPIKey: "456",
EnvEndpoint: ":",
},
expected: `zoneee: parse ":": missing protocol scheme`,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
envTest.Apply(test.envVars)
p, err := NewDNSProvider()
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
apiUser string
apiKey string
endpoint string
expected string
}{
{
desc: "success",
apiKey: "123",
apiUser: "456",
},
{
desc: "missing credentials",
expected: "zoneee: credentials missing: username",
},
{
desc: "missing api key",
apiUser: "456",
expected: "zoneee: credentials missing: API key",
},
{
desc: "missing username",
apiKey: "123",
expected: "zoneee: credentials missing: username",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.APIKey = test.apiKey
config.Username = test.apiUser
if test.endpoint != "" {
config.Endpoint = mustParse(test.endpoint)
}
p, err := NewDNSProviderConfig(config)
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.config)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestDNSProvider_Present(t *testing.T) {
hostedZone := "example.com"
domain := "prefix." + hostedZone
testCases := []struct {
desc string
username string
apiKey string
handlers map[string]http.HandlerFunc
expectedError string
}{
{
desc: "success",
username: "bar",
apiKey: "foo",
handlers: map[string]http.HandlerFunc{
path.Join("/", "dns", hostedZone, "txt"): mockHandlerCreateRecord,
},
},
{
desc: "invalid auth",
username: "nope",
apiKey: "foo",
handlers: map[string]http.HandlerFunc{
path.Join("/", "dns", hostedZone, "txt"): mockHandlerCreateRecord,
},
expectedError: "zoneee: unexpected status code: [status code: 401] body: Unauthorized",
},
{
desc: "error",
username: "bar",
apiKey: "foo",
expectedError: "zoneee: unexpected status code: [status code: 404] body: 404 page not found",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
mux := http.NewServeMux()
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
for uri, handler := range test.handlers {
mux.HandleFunc(uri, handler)
}
config := NewDefaultConfig()
config.Endpoint = mustParse(server.URL)
config.Username = test.username
config.APIKey = test.apiKey
p, err := NewDNSProviderConfig(config)
require.NoError(t, err)
err = p.Present(domain, "token", "key")
if test.expectedError == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, test.expectedError)
}
})
}
}
func TestDNSProvider_Cleanup(t *testing.T) {
hostedZone := "example.com"
domain := "prefix." + hostedZone
testCases := []struct {
desc string
username string
apiKey string
handlers map[string]http.HandlerFunc
expectedError string
}{
{
desc: "success",
username: "bar",
apiKey: "foo",
handlers: map[string]http.HandlerFunc{
path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{{
ID: "1234",
Name: domain,
Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM",
Delete: true,
Modify: true,
}}),
path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord,
},
},
{
desc: "no txt records",
username: "bar",
apiKey: "foo",
handlers: map[string]http.HandlerFunc{
path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{}),
path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord,
},
expectedError: "zoneee: txt record does not exist for LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM",
},
{
desc: "invalid auth",
username: "nope",
apiKey: "foo",
handlers: map[string]http.HandlerFunc{
path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{{
ID: "1234",
Name: domain,
Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM",
Delete: true,
Modify: true,
}}),
path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord,
},
expectedError: "zoneee: unexpected status code: [status code: 401] body: Unauthorized",
},
{
desc: "error",
username: "bar",
apiKey: "foo",
expectedError: "zoneee: unexpected status code: [status code: 404] body: 404 page not found",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
mux := http.NewServeMux()
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
for uri, handler := range test.handlers {
mux.HandleFunc(uri, handler)
}
config := NewDefaultConfig()
config.Endpoint = mustParse(server.URL)
config.Username = test.username
config.APIKey = test.apiKey
p, err := NewDNSProviderConfig(config)
require.NoError(t, err)
err = p.CleanUp(domain, "token", "key")
if test.expectedError == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, test.expectedError)
}
})
}
}
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
err = provider.Present(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func TestLiveCleanUp(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
}
envTest.RestoreEnv()
provider, err := NewDNSProvider()
require.NoError(t, err)
time.Sleep(2 * time.Second)
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
func mustParse(rawURL string) *url.URL {
uri, err := url.Parse(rawURL)
if err != nil {
panic(err)
}
return uri
}
func mockHandlerCreateRecord(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
username, apiKey, ok := req.BasicAuth()
if username != "bar" || apiKey != "foo" || !ok {
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key."))
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
record := internal.TXTRecord{}
err := json.NewDecoder(req.Body).Decode(&record)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
record.ID = "1234"
record.Delete = true
record.Modify = true
record.ResourceURL = req.URL.String() + "/1234"
bytes, err := json.Marshal([]internal.TXTRecord{record})
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
if _, err = rw.Write(bytes); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}
func mockHandlerGetRecords(records []internal.TXTRecord) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
username, apiKey, ok := req.BasicAuth()
if username != "bar" || apiKey != "foo" || !ok {
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key."))
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
for _, value := range records {
if value.ResourceURL == "" {
value.ResourceURL = req.URL.String() + "/" + value.ID
}
}
bytes, err := json.Marshal(records)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
if _, err = rw.Write(bytes); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}
}
func mockHandlerDeleteRecord(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodDelete {
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
username, apiKey, ok := req.BasicAuth()
if username != "bar" || apiKey != "foo" || !ok {
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key."))
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
rw.WriteHeader(http.StatusNoContent)
}