From ad612e639e6cc8f6b4dbc73f0fb123dad690aef7 Mon Sep 17 00:00:00 2001
From: Ludovic Fernandez <ldez@users.noreply.github.com>
Date: Wed, 1 Feb 2023 14:22:25 +0100
Subject: [PATCH] feat: Add DNS provider for Websupport (#1824)

---
 README.md                                     |   4 +-
 cmd/zz_gen_cmd_dnshelp.go                     |  23 ++
 docs/content/dns/zz_gen_websupport.md         |  70 +++++
 docs/data/zz_cli_help.toml                    |   2 +-
 providers/dns/dns_providers.go                |   7 +-
 providers/dns/websupport/internal/client.go   | 258 ++++++++++++++++++
 .../dns/websupport/internal/client_test.go    | 232 ++++++++++++++++
 .../fixtures/add-record-error-400.json        |  26 ++
 .../fixtures/add-record-error-404.json        |   4 +
 .../internal/fixtures/add-record.json         |  19 ++
 .../fixtures/delete-record-error-404.json     |   4 +
 .../internal/fixtures/delete-record.json      |  19 ++
 .../internal/fixtures/get-record.json         |  12 +
 .../internal/fixtures/get-user.json           |  36 +++
 .../internal/fixtures/list-records.json       |  29 ++
 providers/dns/websupport/internal/types.go    | 121 ++++++++
 providers/dns/websupport/websupport.go        | 194 +++++++++++++
 providers/dns/websupport/websupport.toml      |  25 ++
 providers/dns/websupport/websupport_test.go   | 141 ++++++++++
 19 files changed, 1221 insertions(+), 5 deletions(-)
 create mode 100644 docs/content/dns/zz_gen_websupport.md
 create mode 100644 providers/dns/websupport/internal/client.go
 create mode 100644 providers/dns/websupport/internal/client_test.go
 create mode 100644 providers/dns/websupport/internal/fixtures/add-record-error-400.json
 create mode 100644 providers/dns/websupport/internal/fixtures/add-record-error-404.json
 create mode 100644 providers/dns/websupport/internal/fixtures/add-record.json
 create mode 100644 providers/dns/websupport/internal/fixtures/delete-record-error-404.json
 create mode 100644 providers/dns/websupport/internal/fixtures/delete-record.json
 create mode 100644 providers/dns/websupport/internal/fixtures/get-record.json
 create mode 100644 providers/dns/websupport/internal/fixtures/get-user.json
 create mode 100644 providers/dns/websupport/internal/fixtures/list-records.json
 create mode 100644 providers/dns/websupport/internal/types.go
 create mode 100644 providers/dns/websupport/websupport.go
 create mode 100644 providers/dns/websupport/websupport.toml
 create mode 100644 providers/dns/websupport/websupport_test.go

diff --git a/README.md b/README.md
index 80f2f013..14ce6184 100644
--- a/README.md
+++ b/README.md
@@ -75,8 +75,8 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
 | [Sonic](https://go-acme.github.io/lego/dns/sonic/)                              | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/)                      | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/)           | [TransIP](https://go-acme.github.io/lego/dns/transip/)                          |
 | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/)                   | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/)                        | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/)                    | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/)                          |
 | [Vercel](https://go-acme.github.io/lego/dns/vercel/)                            | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/)                 | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/)                        | [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/)                         |
-| [Vscale](https://go-acme.github.io/lego/dns/vscale/)                            | [Vultr](https://go-acme.github.io/lego/dns/vultr/)                              | [WEDOS](https://go-acme.github.io/lego/dns/wedos/)                              | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/)                 |
-| [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/)                        | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/)                           | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/)                            |                                                                                 |
+| [Vscale](https://go-acme.github.io/lego/dns/vscale/)                            | [Vultr](https://go-acme.github.io/lego/dns/vultr/)                              | [Websupport](https://go-acme.github.io/lego/dns/websupport/)                    | [WEDOS](https://go-acme.github.io/lego/dns/wedos/)                              |
+| [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/)                 | [Yandex PDD](https://go-acme.github.io/lego/dns/yandex/)                        | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/)                           | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/)                            |
 
 <!-- END DNS PROVIDERS LIST -->
 
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index cb4e8f14..431ea7b2 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -119,6 +119,7 @@ func allDNSCodes() string {
 		"vkcloud",
 		"vscale",
 		"vultr",
+		"websupport",
 		"wedos",
 		"yandex",
 		"yandexcloud",
@@ -2367,6 +2368,28 @@ func displayDNSHelp(w io.Writer, name string) error {
 		ew.writeln()
 		ew.writeln(`More information: https://go-acme.github.io/lego/dns/vultr`)
 
+	case "websupport":
+		// generated from: providers/dns/websupport/websupport.toml
+		ew.writeln(`Configuration for Websupport.`)
+		ew.writeln(`Code:	'websupport'`)
+		ew.writeln(`Since:	'v4.10.0'`)
+		ew.writeln()
+
+		ew.writeln(`Credentials:`)
+		ew.writeln(`	- "WEBSUPPORT_API_KEY":	API key`)
+		ew.writeln(`	- "WEBSUPPORT_SECRET":	API secret`)
+		ew.writeln()
+
+		ew.writeln(`Additional Configuration:`)
+		ew.writeln(`	- "WEBSUPPORT_HTTP_TIMEOUT":	API request timeout`)
+		ew.writeln(`	- "WEBSUPPORT_POLLING_INTERVAL":	Time between DNS propagation check`)
+		ew.writeln(`	- "WEBSUPPORT_PROPAGATION_TIMEOUT":	Maximum waiting time for DNS propagation`)
+		ew.writeln(`	- "WEBSUPPORT_SEQUENCE_INTERVAL":	Time between sequential requests`)
+		ew.writeln(`	- "WEBSUPPORT_TTL":	The TTL of the TXT record used for the DNS challenge`)
+
+		ew.writeln()
+		ew.writeln(`More information: https://go-acme.github.io/lego/dns/websupport`)
+
 	case "wedos":
 		// generated from: providers/dns/wedos/wedos.toml
 		ew.writeln(`Configuration for WEDOS.`)
diff --git a/docs/content/dns/zz_gen_websupport.md b/docs/content/dns/zz_gen_websupport.md
new file mode 100644
index 00000000..cf850e58
--- /dev/null
+++ b/docs/content/dns/zz_gen_websupport.md
@@ -0,0 +1,70 @@
+---
+title: "Websupport"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: websupport
+dnsprovider:
+  since:    "v4.10.0"
+  code:     "websupport"
+  url:      "https://websupport.sk"
+---
+
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+<!-- providers/dns/websupport/websupport.toml -->
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+
+
+Configuration for [Websupport](https://websupport.sk).
+
+
+<!--more-->
+
+- Code: `websupport`
+- Since: v4.10.0
+
+
+Here is an example bash command using the Websupport provider:
+
+```bash
+WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \
+lego --email myemail@example.com --dns websupport --domains my.example.org run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `WEBSUPPORT_API_KEY` | API key |
+| `WEBSUPPORT_SECRET` | API secret |
+
+The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
+More information [here]({{< ref "dns#configuration-and-credentials" >}}).
+
+
+## Additional Configuration
+
+| Environment Variable Name | Description |
+|--------------------------------|-------------|
+| `WEBSUPPORT_HTTP_TIMEOUT` | API request timeout |
+| `WEBSUPPORT_POLLING_INTERVAL` | Time between DNS propagation check |
+| `WEBSUPPORT_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
+| `WEBSUPPORT_SEQUENCE_INTERVAL` | Time between sequential requests |
+| `WEBSUPPORT_TTL` | The TTL of the TXT record used for the DNS challenge |
+
+The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
+More information [here]({{< ref "dns#configuration-and-credentials" >}}).
+
+
+
+
+## More information
+
+- [API documentation](https://rest.websupport.sk/docs/v1.zone)
+
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+<!-- providers/dns/websupport/websupport.toml -->
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml
index d9926c2a..f526b603 100644
--- a/docs/data/zz_cli_help.toml
+++ b/docs/data/zz_cli_help.toml
@@ -125,7 +125,7 @@ To display the documentation for a specific DNS provider, run:
   $ lego dnshelp -c code
 
 Supported DNS providers:
-  acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, bindman, bluecat, checkdomain, civo, clouddns, cloudflare, cloudns, cloudxns, conoha, constellix, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, hetzner, hostingde, hosttech, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, ns1, oraclecloud, otc, ovh, pdns, porkbun, rackspace, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, wedos, yandex, yandexcloud, zoneee, zonomi
+  acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, bindman, bluecat, checkdomain, civo, clouddns, cloudflare, cloudns, cloudxns, conoha, constellix, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, hetzner, hostingde, hosttech, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, manual, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, ns1, oraclecloud, otc, ovh, pdns, porkbun, rackspace, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, websupport, wedos, yandex, yandexcloud, zoneee, zonomi
 
 More information: https://go-acme.github.io/lego/dns
 """
diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go
index a0343fb7..2d7b7c1c 100644
--- a/providers/dns/dns_providers.go
+++ b/providers/dns/dns_providers.go
@@ -110,6 +110,7 @@ import (
 	"github.com/go-acme/lego/v4/providers/dns/vkcloud"
 	"github.com/go-acme/lego/v4/providers/dns/vscale"
 	"github.com/go-acme/lego/v4/providers/dns/vultr"
+	"github.com/go-acme/lego/v4/providers/dns/websupport"
 	"github.com/go-acme/lego/v4/providers/dns/wedos"
 	"github.com/go-acme/lego/v4/providers/dns/yandex"
 	"github.com/go-acme/lego/v4/providers/dns/yandexcloud"
@@ -328,10 +329,12 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
 		return vinyldns.NewDNSProvider()
 	case "vkcloud":
 		return vkcloud.NewDNSProvider()
-	case "vultr":
-		return vultr.NewDNSProvider()
 	case "vscale":
 		return vscale.NewDNSProvider()
+	case "vultr":
+		return vultr.NewDNSProvider()
+	case "websupport":
+		return websupport.NewDNSProvider()
 	case "wedos":
 		return wedos.NewDNSProvider()
 	case "yandex":
diff --git a/providers/dns/websupport/internal/client.go b/providers/dns/websupport/internal/client.go
new file mode 100644
index 00000000..1dc2d42b
--- /dev/null
+++ b/providers/dns/websupport/internal/client.go
@@ -0,0 +1,258 @@
+package internal
+
+import (
+	"bytes"
+	"crypto/hmac"
+	"crypto/sha1"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"path"
+	"strconv"
+	"time"
+)
+
+const defaultBaseURL = "https://rest.websupport.sk"
+
+// StatusSuccess expected status text when success.
+const StatusSuccess = "success"
+
+// Client a Websupport DNS API client.
+type Client struct {
+	apiKey     string
+	secretKey  string
+	BaseURL    string
+	HTTPClient *http.Client
+}
+
+// NewClient creates a new Client.
+func NewClient(apiKey, secretKey string) (*Client, error) {
+	if apiKey == "" || secretKey == "" {
+		return nil, errors.New("credentials missing")
+	}
+
+	return &Client{
+		apiKey:     apiKey,
+		secretKey:  secretKey,
+		BaseURL:    defaultBaseURL,
+		HTTPClient: &http.Client{Timeout: 10 * time.Second},
+	}, nil
+}
+
+// GetUser gets a user detail.
+// https://rest.websupport.sk/docs/v1.user#user
+func (c *Client) GetUser(userID string) (*User, error) {
+	baseURL, err := url.Parse(c.BaseURL)
+	if err != nil {
+		return nil, fmt.Errorf("base url parsing: %w", err)
+	}
+
+	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", userID))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse endpoint: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody)
+	if err != nil {
+		return nil, fmt.Errorf("request payload: %w", err)
+	}
+
+	result := &User{}
+
+	err = c.do(req, result)
+	if err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// ListRecords lists all records.
+// https://rest.websupport.sk/docs/v1.zone#records
+func (c *Client) ListRecords(domainName string) (*ListResponse, error) {
+	baseURL, err := url.Parse(c.BaseURL)
+	if err != nil {
+		return nil, fmt.Errorf("base url parsing: %w", err)
+	}
+
+	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record"))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse endpoint: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody)
+	if err != nil {
+		return nil, fmt.Errorf("request payload: %w", err)
+	}
+
+	result := &ListResponse{}
+
+	err = c.do(req, result)
+	if err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// GetRecords gets a DNS record.
+func (c *Client) GetRecords(domainName string, recordID int) (*Record, error) {
+	baseURL, err := url.Parse(c.BaseURL)
+	if err != nil {
+		return nil, fmt.Errorf("base url parsing: %w", err)
+	}
+
+	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record", strconv.Itoa(recordID)))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse endpoint: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody)
+	if err != nil {
+		return nil, err
+	}
+
+	result := &Record{}
+
+	err = c.do(req, result)
+	if err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// AddRecord adds a DNS record.
+// https://rest.websupport.sk/docs/v1.zone#post-record
+func (c *Client) AddRecord(domainName string, record Record) (*Response, error) {
+	baseURL, err := url.Parse(c.BaseURL)
+	if err != nil {
+		return nil, fmt.Errorf("base url parsing: %w", err)
+	}
+
+	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record"))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse endpoint: %w", err)
+	}
+
+	payload, err := json.Marshal(record)
+	if err != nil {
+		return nil, fmt.Errorf("request payload: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader(payload))
+	if err != nil {
+		return nil, err
+	}
+
+	result := &Response{}
+
+	err = c.do(req, result)
+	if err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// DeleteRecord deletes a DNS record.
+// https://rest.websupport.sk/docs/v1.zone#delete-record
+func (c *Client) DeleteRecord(domainName string, recordID int) (*Response, error) {
+	baseURL, err := url.Parse(c.BaseURL)
+	if err != nil {
+		return nil, fmt.Errorf("base url parsing: %w", err)
+	}
+
+	endpoint, err := baseURL.Parse(path.Join(baseURL.Path, "v1", "user", "self", "zone", domainName, "record", strconv.Itoa(recordID)))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse endpoint: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodDelete, endpoint.String(), http.NoBody)
+	if err != nil {
+		return nil, fmt.Errorf("request payload: %w", err)
+	}
+
+	result := &Response{}
+
+	err = c.do(req, result)
+	if err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+func (c *Client) do(req *http.Request, result any) error {
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/json")
+	req.Header.Set("Accept-Language", "en_us")
+
+	location, err := time.LoadLocation("GMT")
+	if err != nil {
+		return fmt.Errorf("time location: %w", err)
+	}
+
+	err = c.sign(req, time.Now().In(location))
+	if err != nil {
+		return fmt.Errorf("signature: %w", err)
+	}
+
+	resp, err := c.HTTPClient.Do(req)
+	if err != nil {
+		return err
+	}
+
+	defer func() { _ = resp.Body.Close() }()
+
+	if resp.StatusCode > http.StatusBadRequest {
+		all, _ := io.ReadAll(resp.Body)
+
+		var e APIError
+		err = json.Unmarshal(all, &e)
+		if err != nil {
+			return fmt.Errorf("%d: %s", resp.StatusCode, string(all))
+		}
+
+		return &e
+	}
+
+	all, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("read response body: %w", err)
+	}
+
+	err = json.Unmarshal(all, result)
+	if err != nil {
+		return fmt.Errorf("unmarshal response body: %w", err)
+	}
+
+	return nil
+}
+
+func (c *Client) sign(req *http.Request, now time.Time) error {
+	if req.URL.Path == "" {
+		req.URL.Path += "/"
+	}
+
+	canonicalRequest := fmt.Sprintf("%s %s %d", req.Method, req.URL.Path, now.Unix())
+
+	mac := hmac.New(sha1.New, []byte(c.secretKey))
+	_, err := mac.Write([]byte(canonicalRequest))
+	if err != nil {
+		return err
+	}
+
+	hashed := mac.Sum(nil)
+	signature := hex.EncodeToString(hashed)
+
+	req.SetBasicAuth(c.apiKey, signature)
+
+	req.Header.Set("Date", now.Format(time.RFC3339))
+
+	return nil
+}
diff --git a/providers/dns/websupport/internal/client_test.go b/providers/dns/websupport/internal/client_test.go
new file mode 100644
index 00000000..312d74f3
--- /dev/null
+++ b/providers/dns/websupport/internal/client_test.go
@@ -0,0 +1,232 @@
+package internal
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
+	t.Helper()
+
+	mux := http.NewServeMux()
+	server := httptest.NewServer(mux)
+	t.Cleanup(server.Close)
+
+	mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
+		if req.Method != method {
+			http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
+			return
+		}
+
+		open, err := os.Open(file)
+		if err != nil {
+			http.Error(rw, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		defer func() { _ = open.Close() }()
+
+		rw.WriteHeader(status)
+		_, err = io.Copy(rw, open)
+		if err != nil {
+			http.Error(rw, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	})
+
+	client, err := NewClient("apiKey", "secretKey")
+	require.NoError(t, err)
+
+	client.HTTPClient = server.Client()
+	client.BaseURL = server.URL
+
+	return client
+}
+
+func TestClient_GetUser(t *testing.T) {
+	client := setupTest(t, http.MethodGet, "/v1/user/self", http.StatusOK, "./fixtures/get-user.json")
+
+	user, err := client.GetUser("self")
+	require.NoError(t, err)
+
+	expected := &User{
+		ID:                      987654321,
+		Login:                   "lego@example.com",
+		Active:                  true,
+		CreateTime:              1675237889,
+		Group:                   "users",
+		Email:                   "lego@example.com",
+		Phone:                   "+123456789",
+		ContactPerson:           "",
+		AwaitingTosConfirmation: "1",
+		UserLanguage:            "sk-SK",
+		Credit:                  0,
+		VerifyURL:               "https://rest.websupport.sk/v1/user/verify/key/xxx",
+		Billing: []Billing{{
+			ID:        1099970,
+			Profile:   "default",
+			IsDefault: true,
+			Name:      "asdsdfs",
+			City:      "Žilina",
+			Street:    "asddfsdfsdf",
+			Zip:       "01234",
+			Country:   "sk",
+		}},
+		Market: Market{Name: "Slovakia", Identifier: "sk", Currency: "EUR"},
+	}
+
+	assert.Equal(t, expected, user)
+}
+
+func TestClient_ListRecords(t *testing.T) {
+	client := setupTest(t, http.MethodGet, "/v1/user/self/zone/example.com/record", http.StatusOK, "./fixtures/list-records.json")
+
+	resp, err := client.ListRecords("example.com")
+	require.NoError(t, err)
+
+	expected := &ListResponse{
+		Items: []Record{
+			{
+				ID:      1,
+				Type:    "A",
+				Name:    "@",
+				Content: "37.9.169.99",
+				TTL:     600,
+			}, {
+				ID:      2,
+				Type:    "NS",
+				Name:    "@",
+				Content: "ns1.scaledo.com",
+				TTL:     600,
+			},
+		},
+		Pager: Pager{Page: 1, PageSize: 0, Items: 2},
+	}
+
+	assert.Equal(t, expected, resp)
+}
+
+func TestClient_AddRecord(t *testing.T) {
+	client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusCreated, "./fixtures/add-record.json")
+
+	record := Record{
+		Type:    "TXT",
+		Name:    "_acme-challenge",
+		Content: "txttxttxt",
+		TTL:     600,
+	}
+
+	resp, err := client.AddRecord("example.com", record)
+	require.NoError(t, err)
+
+	expected := &Response{
+		Status: "success",
+		Item: &Record{
+			ID:      4,
+			Type:    "A",
+			Name:    "@",
+			Content: "1.2.3.4",
+			TTL:     600,
+			Zone: &Zone{
+				ID:         1,
+				Name:       "example.com",
+				UpdateTime: 1381169608,
+			},
+		},
+		Errors: json.RawMessage("[]"),
+	}
+
+	assert.Equal(t, expected, resp)
+}
+
+func TestClient_AddRecord_error_400(t *testing.T) {
+	client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusBadRequest, "./fixtures/add-record-error-400.json")
+
+	record := Record{
+		Type:    "TXT",
+		Name:    "_acme-challenge",
+		Content: "txttxttxt",
+		TTL:     600,
+	}
+
+	resp, err := client.AddRecord("example.com", record)
+	require.NoError(t, err)
+
+	assert.Equal(t, "error", resp.Status)
+
+	expectedRecord := &Record{
+		ID:      0,
+		Type:    "A",
+		Name:    "something bad !@#$%^&*(",
+		Content: "123.456.789.123",
+		TTL:     600,
+		Zone: &Zone{
+			ID:         1,
+			Name:       "scaledo.com",
+			UpdateTime: 1381169608,
+		},
+	}
+	assert.Equal(t, expectedRecord, resp.Item)
+
+	expected := &Errors{Name: []string{"Invalid input."}, Content: []string{"Wrong IP address format"}}
+	assert.Equal(t, expected, ParseError(resp))
+}
+
+func TestClient_AddRecord_error_404(t *testing.T) {
+	client := setupTest(t, http.MethodPost, "/v1/user/self/zone/example.com/record", http.StatusNotFound, "./fixtures/add-record-error-404.json")
+
+	record := Record{
+		Type:    "TXT",
+		Name:    "_acme-challenge",
+		Content: "txttxttxt",
+		TTL:     600,
+	}
+
+	resp, err := client.AddRecord("example.com", record)
+	require.Error(t, err)
+
+	assert.Nil(t, resp)
+}
+
+func TestClient_DeleteRecord(t *testing.T) {
+	client := setupTest(t, http.MethodDelete, "/v1/user/self/zone/example.com/record/123", http.StatusOK, "./fixtures/delete-record.json")
+
+	resp, err := client.DeleteRecord("example.com", 123)
+	require.NoError(t, err)
+
+	expected := &Response{
+		Status: "success",
+		Item: &Record{
+			ID:      1,
+			Type:    "A",
+			Name:    "@",
+			Content: "1.2.3.4",
+			TTL:     600,
+			Zone: &Zone{
+				ID:         1,
+				Name:       "scaledo.com",
+				UpdateTime: 1381316081,
+			},
+		},
+		Errors: json.RawMessage("[]"),
+	}
+
+	assert.Equal(t, expected, resp)
+}
+
+func TestClient_DeleteRecord_error(t *testing.T) {
+	client := setupTest(t, http.MethodDelete, "/v1/user/self/zone/example.com/record/123", http.StatusNotFound, "./fixtures/delete-record-error-404.json")
+
+	resp, err := client.DeleteRecord("example.com", 123)
+	require.Error(t, err)
+
+	assert.Nil(t, resp)
+}
diff --git a/providers/dns/websupport/internal/fixtures/add-record-error-400.json b/providers/dns/websupport/internal/fixtures/add-record-error-400.json
new file mode 100644
index 00000000..b60b7989
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/add-record-error-400.json
@@ -0,0 +1,26 @@
+{
+  "status": "error",
+  "item": {
+    "id": null,
+    "type": "A",
+    "name": "something bad !@#$%^&*(",
+    "content": "123.456.789.123",
+    "ttl": 600,
+    "prio": null,
+    "weight": null,
+    "port": null,
+    "zone": {
+      "id": 1,
+      "name": "scaledo.com",
+      "updateTime": 1381169608
+    }
+  },
+  "errors": {
+    "content": [
+      "Wrong IP address format"
+    ],
+    "name": [
+      "Invalid input."
+    ]
+  }
+}
diff --git a/providers/dns/websupport/internal/fixtures/add-record-error-404.json b/providers/dns/websupport/internal/fixtures/add-record-error-404.json
new file mode 100644
index 00000000..837b5392
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/add-record-error-404.json
@@ -0,0 +1,4 @@
+{
+  "code": 404,
+  "message": "Zone not found"
+}
diff --git a/providers/dns/websupport/internal/fixtures/add-record.json b/providers/dns/websupport/internal/fixtures/add-record.json
new file mode 100644
index 00000000..5990cf3d
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/add-record.json
@@ -0,0 +1,19 @@
+{
+  "status": "success",
+  "item": {
+    "id": 4,
+    "type": "A",
+    "name": "@",
+    "content": "1.2.3.4",
+    "ttl": 600,
+    "prio": null,
+    "weight": null,
+    "port": null,
+    "zone": {
+      "id": 1,
+      "name": "example.com",
+      "updateTime": 1381169608
+    }
+  },
+  "errors": []
+}
diff --git a/providers/dns/websupport/internal/fixtures/delete-record-error-404.json b/providers/dns/websupport/internal/fixtures/delete-record-error-404.json
new file mode 100644
index 00000000..e66fa5dc
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/delete-record-error-404.json
@@ -0,0 +1,4 @@
+{
+  "code": 404,
+  "message": "Record not found"
+}
diff --git a/providers/dns/websupport/internal/fixtures/delete-record.json b/providers/dns/websupport/internal/fixtures/delete-record.json
new file mode 100644
index 00000000..8fdff82c
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/delete-record.json
@@ -0,0 +1,19 @@
+{
+  "status": "success",
+  "item": {
+    "id": 1,
+    "type": "A",
+    "name": "@",
+    "content": "1.2.3.4",
+    "ttl": 600,
+    "prio": null,
+    "weight": null,
+    "port": null,
+    "zone": {
+      "id": 1,
+      "name": "scaledo.com",
+      "updateTime": 1381316081
+    }
+  },
+  "errors": []
+}
diff --git a/providers/dns/websupport/internal/fixtures/get-record.json b/providers/dns/websupport/internal/fixtures/get-record.json
new file mode 100644
index 00000000..d1bd2f13
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/get-record.json
@@ -0,0 +1,12 @@
+{
+  "id": 69966832,
+  "type": "TXT",
+  "name": "_acme-challenge",
+  "content": "txttxttxt",
+  "ttl": 600,
+  "zone": {
+    "id": 0,
+    "name": "example.com",
+    "updateTime": 1675240207
+  }
+}
diff --git a/providers/dns/websupport/internal/fixtures/get-user.json b/providers/dns/websupport/internal/fixtures/get-user.json
new file mode 100644
index 00000000..ad497875
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/get-user.json
@@ -0,0 +1,36 @@
+{
+  "id": 987654321,
+  "login": "lego@example.com",
+  "parentId": null,
+  "active": true,
+  "createTime": 1675237889,
+  "group": "users",
+  "email": "lego@example.com",
+  "phone": "+123456789",
+  "contactPerson": "",
+  "awaitingTosConfirmation": "1",
+  "userLanguage": "sk-SK",
+  "credit": 0,
+  "verifyUrl": "https:\/\/rest.websupport.sk\/v1\/user\/verify\/key\/xxx",
+  "billing": [
+    {
+      "id": 1099970,
+      "profile": "default",
+      "isDefault": true,
+      "name": "asdsdfs",
+      "city": "\u017dilina",
+      "street": "asddfsdfsdf",
+      "companyRegId": null,
+      "taxId": null,
+      "vatId": null,
+      "zip": "01234",
+      "country": "sk",
+      "isic": ""
+    }
+  ],
+  "market": {
+    "name": "Slovakia",
+    "identifier": "sk",
+    "currency": "EUR"
+  }
+}
diff --git a/providers/dns/websupport/internal/fixtures/list-records.json b/providers/dns/websupport/internal/fixtures/list-records.json
new file mode 100644
index 00000000..d0ad57dc
--- /dev/null
+++ b/providers/dns/websupport/internal/fixtures/list-records.json
@@ -0,0 +1,29 @@
+{
+  "items": [
+    {
+      "id": 1,
+      "type": "A",
+      "name": "@",
+      "content": "37.9.169.99",
+      "ttl": 600,
+      "prio": null,
+      "weight": null,
+      "port": null
+    },
+    {
+      "id": 2,
+      "type": "NS",
+      "name": "@",
+      "content": "ns1.scaledo.com",
+      "ttl": 600,
+      "prio": null,
+      "weight": null,
+      "port": null
+    }
+  ],
+  "pager": {
+    "page": 1,
+    "pagesize": 0,
+    "items": 2
+  }
+}
diff --git a/providers/dns/websupport/internal/types.go b/providers/dns/websupport/internal/types.go
new file mode 100644
index 00000000..cada90ce
--- /dev/null
+++ b/providers/dns/websupport/internal/types.go
@@ -0,0 +1,121 @@
+package internal
+
+import (
+	"encoding/json"
+	"fmt"
+)
+
+type APIError struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+}
+
+func (a *APIError) Error() string {
+	return fmt.Sprintf("%d: %s", a.Code, a.Message)
+}
+
+type Record struct {
+	ID      int    `json:"id,omitempty"`
+	Type    string `json:"type,omitempty"`
+	Name    string `json:"name,omitempty"` // subdomain name or @ if you don't want subdomain
+	Content string `json:"content,omitempty"`
+	TTL     int    `json:"ttl,omitempty"` // default 600
+	Zone    *Zone  `json:"zone"`
+}
+
+type Zone struct {
+	ID         int    `json:"id"`
+	Name       string `json:"name"`
+	UpdateTime int    `json:"updateTime"`
+}
+
+type Response struct {
+	Status string          `json:"status"`
+	Item   *Record         `json:"item"`
+	Errors json.RawMessage `json:"errors"`
+}
+
+type ListResponse struct {
+	Items []Record `json:"items"`
+	Pager Pager    `json:"pager"`
+}
+
+type Pager struct {
+	Page     int `json:"page"`
+	PageSize int `json:"pagesize"`
+	Items    int `json:"items"`
+}
+
+type Errors struct {
+	Name    []string `json:"name"`
+	Content []string `json:"content"`
+}
+
+func (e *Errors) Error() string {
+	var msg string
+	for i, s := range e.Name {
+		msg += s
+		if i != len(e.Name)-1 {
+			msg += ": "
+		}
+	}
+
+	for i, s := range e.Content {
+		msg += s
+		if i != len(e.Content)-1 {
+			msg += ": "
+		}
+	}
+
+	return msg
+}
+
+// ParseError extract error from Response.
+func ParseError(resp *Response) error {
+	var apiError Errors
+	err := json.Unmarshal(resp.Errors, &apiError)
+	if err != nil {
+		return err
+	}
+
+	return &apiError
+}
+
+type User struct {
+	ID                      int       `json:"id"`
+	Login                   string    `json:"login"`
+	ParentID                int       `json:"parentId"`
+	Active                  bool      `json:"active"`
+	CreateTime              int       `json:"createTime"`
+	Group                   string    `json:"group"`
+	Email                   string    `json:"email"`
+	Phone                   string    `json:"phone"`
+	ContactPerson           string    `json:"contactPerson"`
+	AwaitingTosConfirmation string    `json:"awaitingTosConfirmation"`
+	UserLanguage            string    `json:"userLanguage"`
+	Credit                  int       `json:"credit"`
+	VerifyURL               string    `json:"verifyUrl"`
+	Billing                 []Billing `json:"billing"`
+	Market                  Market    `json:"market"`
+}
+
+type Billing struct {
+	ID           int    `json:"id"`
+	Profile      string `json:"profile"`
+	IsDefault    bool   `json:"isDefault"`
+	Name         string `json:"name"`
+	City         string `json:"city"`
+	Street       string `json:"street"`
+	CompanyRegID int    `json:"companyRegId"`
+	TaxID        int    `json:"taxId"`
+	VatID        int    `json:"vatId"`
+	Zip          string `json:"zip"`
+	Country      string `json:"country"`
+	ISIC         string `json:"isic"`
+}
+
+type Market struct {
+	Name       string `json:"name"`
+	Identifier string `json:"identifier"`
+	Currency   string `json:"currency"`
+}
diff --git a/providers/dns/websupport/websupport.go b/providers/dns/websupport/websupport.go
new file mode 100644
index 00000000..1af3618f
--- /dev/null
+++ b/providers/dns/websupport/websupport.go
@@ -0,0 +1,194 @@
+// Package websupport implements a DNS provider for solving the DNS-01 challenge using Websupport.
+package websupport
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/go-acme/lego/v4/challenge/dns01"
+	"github.com/go-acme/lego/v4/platform/config/env"
+	"github.com/go-acme/lego/v4/providers/dns/websupport/internal"
+)
+
+const defaultTTL = 600
+
+// Environment variables names.
+const (
+	envNamespace = "WEBSUPPORT_"
+
+	EnvAPIKey = envNamespace + "API_KEY"
+	EnvSecret = envNamespace + "SECRET"
+
+	EnvTTL                = envNamespace + "TTL"
+	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
+	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
+	EnvSequenceInterval   = envNamespace + "SEQUENCE_INTERVAL"
+)
+
+// Config is used to configure the creation of the DNSProvider.
+type Config struct {
+	APIKey string
+	Secret string
+
+	PropagationTimeout time.Duration
+	PollingInterval    time.Duration
+	SequenceInterval   time.Duration
+	TTL                int
+	HTTPClient         *http.Client
+}
+
+// NewDefaultConfig returns a default configuration for the DNSProvider.
+func NewDefaultConfig() *Config {
+	return &Config{
+		TTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),
+		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
+		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
+		SequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
+		HTTPClient: &http.Client{
+			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
+		},
+	}
+}
+
+// DNSProvider implements the challenge.Provider interface.
+type DNSProvider struct {
+	config *Config
+	client *internal.Client
+
+	recordIDs   map[string]int
+	recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Websupport.
+// Credentials must be passed in the environment variables: WEBSUPPORT_API_KEY, WEBSUPPORT_SECRET.
+func NewDNSProvider() (*DNSProvider, error) {
+	values, err := env.Get(EnvAPIKey, EnvSecret)
+	if err != nil {
+		return nil, fmt.Errorf("websupport: %w", err)
+	}
+
+	config := NewDefaultConfig()
+	config.APIKey = values[EnvAPIKey]
+	config.Secret = values[EnvSecret]
+
+	return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for Websupport.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+	if config == nil {
+		return nil, errors.New("websupport: the configuration of the DNS provider is nil")
+	}
+
+	client, err := internal.NewClient(config.APIKey, config.Secret)
+	if err != nil {
+		return nil, fmt.Errorf("websupport: %w", err)
+	}
+
+	if config.HTTPClient != nil {
+		client.HTTPClient = config.HTTPClient
+	}
+
+	return &DNSProvider{
+		config:    config,
+		client:    client,
+		recordIDs: make(map[string]int),
+	}, nil
+}
+
+// Present creates a TXT record using the specified parameters.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+	fqdn, value := dns01.GetRecord(domain, keyAuth)
+
+	authZone, err := dns01.FindZoneByFqdn(fqdn)
+	if err != nil {
+		return fmt.Errorf("websupport: %w", err)
+	}
+
+	subDomain, err := dns01.ExtractSubDomain(fqdn, authZone)
+	if err != nil {
+		return fmt.Errorf("websupport: %w", err)
+	}
+
+	record := internal.Record{
+		Type:    "TXT",
+		Name:    subDomain,
+		Content: value,
+		TTL:     d.config.TTL,
+	}
+
+	resp, err := d.client.AddRecord(dns01.UnFqdn(authZone), record)
+	if err != nil {
+		return fmt.Errorf("websupport: add record: %w", err)
+	}
+
+	if resp.Status == internal.StatusSuccess {
+		d.recordIDsMu.Lock()
+		d.recordIDs[token] = resp.Item.ID
+		d.recordIDsMu.Unlock()
+
+		return nil
+	}
+
+	err = internal.ParseError(resp)
+	if err != nil {
+		return fmt.Errorf("websupport: %w", err)
+	}
+
+	return nil
+}
+
+// CleanUp removes the TXT record matching the specified parameters.
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+	fqdn, _ := dns01.GetRecord(domain, keyAuth)
+
+	authZone, err := dns01.FindZoneByFqdn(fqdn)
+	if err != nil {
+		return fmt.Errorf("websupport: %w", err)
+	}
+
+	// gets the record's unique ID
+	d.recordIDsMu.Lock()
+	recordID, ok := d.recordIDs[token]
+	d.recordIDsMu.Unlock()
+	if !ok {
+		return fmt.Errorf("websupport: unknown record ID for '%s' '%s'", fqdn, token)
+	}
+
+	resp, err := d.client.DeleteRecord(dns01.UnFqdn(authZone), recordID)
+	if err != nil {
+		return fmt.Errorf("websupport: delete record: %w", err)
+	}
+
+	// deletes record ID from map
+	d.recordIDsMu.Lock()
+	delete(d.recordIDs, token)
+	d.recordIDsMu.Unlock()
+
+	if resp.Status == internal.StatusSuccess {
+		return nil
+	}
+
+	err = internal.ParseError(resp)
+	if err != nil {
+		return fmt.Errorf("websupport: %w", err)
+	}
+
+	return nil
+}
+
+// Timeout returns the timeout and interval to use when checking for DNS propagation.
+// Adjusting here to cope with spikes in propagation times.
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+	return d.config.PropagationTimeout, d.config.PollingInterval
+}
+
+// Sequential All DNS challenges for this provider will be resolved sequentially.
+// Returns the interval between each iteration.
+func (d *DNSProvider) Sequential() time.Duration {
+	return d.config.SequenceInterval
+}
diff --git a/providers/dns/websupport/websupport.toml b/providers/dns/websupport/websupport.toml
new file mode 100644
index 00000000..8eb32fbb
--- /dev/null
+++ b/providers/dns/websupport/websupport.toml
@@ -0,0 +1,25 @@
+Name = "Websupport"
+Description = ''''''
+URL = "https://websupport.sk"
+Code = "websupport"
+Since = "v4.10.0"
+
+Example = '''
+WEBSUPPORT_API_KEY="xxxxxxxxxxxxxxxxxxxxx" \
+WEBSUPPORT_SECRET="yyyyyyyyyyyyyyyyyyyyy" \
+lego --email myemail@example.com --dns websupport --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    WEBSUPPORT_API_KEY = "API key"
+    WEBSUPPORT_SECRET = "API secret"
+  [Configuration.Additional]
+    WEBSUPPORT_POLLING_INTERVAL = "Time between DNS propagation check"
+    WEBSUPPORT_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    WEBSUPPORT_SEQUENCE_INTERVAL = "Time between sequential requests"
+    WEBSUPPORT_TTL = "The TTL of the TXT record used for the DNS challenge"
+    WEBSUPPORT_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://rest.websupport.sk/docs/v1.zone"
diff --git a/providers/dns/websupport/websupport_test.go b/providers/dns/websupport/websupport_test.go
new file mode 100644
index 00000000..e79dd713
--- /dev/null
+++ b/providers/dns/websupport/websupport_test.go
@@ -0,0 +1,141 @@
+package websupport
+
+import (
+	"testing"
+
+	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/stretchr/testify/require"
+)
+
+const envDomain = envNamespace + "DOMAIN"
+
+var envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret).WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		envVars  map[string]string
+		expected string
+	}{
+		{
+			desc: "success",
+			envVars: map[string]string{
+				EnvAPIKey: "key",
+				EnvSecret: "secret",
+			},
+		},
+		{
+			desc: "missing API key",
+			envVars: map[string]string{
+				EnvSecret: "secret",
+			},
+			expected: "websupport: some credentials information are missing: WEBSUPPORT_API_KEY",
+		},
+		{
+			desc: "missing secret",
+			envVars: map[string]string{
+				EnvAPIKey: "key",
+			},
+			expected: "websupport: some credentials information are missing: WEBSUPPORT_SECRET",
+		},
+		{
+			desc:     "missing credentials",
+			envVars:  map[string]string{},
+			expected: "websupport: some credentials information are missing: WEBSUPPORT_API_KEY,WEBSUPPORT_SECRET",
+		},
+	}
+
+	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)
+				require.NotNil(t, p.client)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+}
+
+func TestNewDNSProviderConfig(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		apiKey   string
+		secret   string
+		expected string
+	}{
+		{
+			desc:   "success",
+			apiKey: "key",
+			secret: "secret",
+		},
+		{
+			desc:     "missing API key",
+			secret:   "secret",
+			expected: "websupport: credentials missing",
+		},
+		{
+			desc:     "missing secret",
+			apiKey:   "key",
+			expected: "websupport: credentials missing",
+		},
+		{
+			desc:     "missing credentials",
+			expected: "websupport: credentials missing",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			config := NewDefaultConfig()
+			config.APIKey = test.apiKey
+			config.Secret = test.secret
+
+			p, err := NewDNSProviderConfig(config)
+
+			if test.expected == "" {
+				require.NoError(t, err)
+				require.NotNil(t, p)
+				require.NotNil(t, p.config)
+				require.NotNil(t, p.client)
+			} else {
+				require.EqualError(t, err, test.expected)
+			}
+		})
+	}
+}
+
+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)
+
+	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
+	require.NoError(t, err)
+}