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