Repository: go-acme/lego (language: go)

Issue:
Add support for Porkbun DNS
<!-- PLEASE FOLLOW THE ISSUE TEMPLATE TO HELP TRIAGE AND SUPPORT! -->

- [x] Yes, I've searched similar issues on GitHub and didn't find any.
- [x] Yes, the DNS provider exposes a public API.
- [x] Yes, I know that the lego maintainers don't have an account in all DNS providers in the world.
- [ ] Yes, I'm able to create a pull request and be able to maintain the implementation
- [x] Yes, I'm able to test an implementation if someone creates a pull request to add the support of this DNS provider. 

### Link to the DNS provider

[https://porkbun.com/](https://porkbun.com/)

### Link to the API documentation

[https://porkbun.com/api/json/v3/documentation](https://porkbun.com/api/json/v3/documentation)

### Additional Notes

The API is pretty new and claims to be in beta, but it seems to have enough basic functionality for DNS challenges. I'm not familiar with Go, but I'm happy to help testing or in any other way. 

acme.sh has it implemented [here](https://github.com/acmesh-official/acme.sh/blob/master/dnsapi/dns_porkbun.sh) if that's useful.



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 3b03a739f7..d3bac1708e 100644
--- a/README.md
+++ b/README.md
@@ -62,11 +62,11 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
 | [Manual](https://go-acme.github.io/lego/dns/manual/)                            | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/)                         | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/)                | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/)                      |
 | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/)                      | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/)                        | [Netcup](https://go-acme.github.io/lego/dns/netcup/)                            | [Netlify](https://go-acme.github.io/lego/dns/netlify/)                          |
 | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/)                        | [Njalla](https://go-acme.github.io/lego/dns/njalla/)                            | [NS1](https://go-acme.github.io/lego/dns/ns1/)                                  | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/)                   |
-| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/)                 | [OVH](https://go-acme.github.io/lego/dns/ovh/)                                  | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/)                            | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/)                      |
-| [reg.ru](https://go-acme.github.io/lego/dns/regru/)                             | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/)                          | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/)                  | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/)                 |
-| [Scaleway](https://go-acme.github.io/lego/dns/scaleway/)                        | [Selectel](https://go-acme.github.io/lego/dns/selectel/)                        | [Servercow](https://go-acme.github.io/lego/dns/servercow/)                      | [Sonic](https://go-acme.github.io/lego/dns/sonic/)                              |
-| [Stackpath](https://go-acme.github.io/lego/dns/stackpath/)                      | [TransIP](https://go-acme.github.io/lego/dns/transip/)                          | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/)                          | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/)                 |
-| [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/)                        | [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](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/)                            |                                                                                 |
+| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/)                 | [OVH](https://go-acme.github.io/lego/dns/ovh/)                                  | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/)                          | [PowerDNS](https://go-acme.github.io/lego/dns/pdns/)                            |
+| [Rackspace](https://go-acme.github.io/lego/dns/rackspace/)                      | [reg.ru](https://go-acme.github.io/lego/dns/regru/)                             | [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/)                          | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/)                  |
+| [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/)                 | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/)                        | [Selectel](https://go-acme.github.io/lego/dns/selectel/)                        | [Servercow](https://go-acme.github.io/lego/dns/servercow/)                      |
+| [Sonic](https://go-acme.github.io/lego/dns/sonic/)                              | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/)                      | [TransIP](https://go-acme.github.io/lego/dns/transip/)                          | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/)                          |
+| [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/)                 | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/)                        | [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](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 22199b0a3b..7e3729a185 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -79,6 +79,7 @@ func allDNSCodes() string {
 		"otc",
 		"ovh",
 		"pdns",
+		"porkbun",
 		"rackspace",
 		"regru",
 		"rfc2136",
@@ -1486,6 +1487,27 @@ func displayDNSHelp(name string) error {
 		ew.writeln()
 		ew.writeln(`More information: https://go-acme.github.io/lego/dns/pdns`)
 
+	case "porkbun":
+		// generated from: providers/dns/porkbun/porkbun.toml
+		ew.writeln(`Configuration for Porkbun.`)
+		ew.writeln(`Code:	'porkbun'`)
+		ew.writeln(`Since:	'v4.4.0'`)
+		ew.writeln()
+
+		ew.writeln(`Credentials:`)
+		ew.writeln(`	- "PORKBUN_API_KEY":	API key`)
+		ew.writeln(`	- "PORKBUN_SECRET_API_KEY":	secret API key`)
+		ew.writeln()
+
+		ew.writeln(`Additional Configuration:`)
+		ew.writeln(`	- "PORKBUN_HTTP_TIMEOUT":	API request timeout`)
+		ew.writeln(`	- "PORKBUN_POLLING_INTERVAL":	Time between DNS propagation check`)
+		ew.writeln(`	- "PORKBUN_PROPAGATION_TIMEOUT":	Maximum waiting time for DNS propagation`)
+		ew.writeln(`	- "PORKBUN_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/porkbun`)
+
 	case "rackspace":
 		// generated from: providers/dns/rackspace/rackspace.toml
 		ew.writeln(`Configuration for Rackspace.`)
diff --git a/docs/content/dns/zz_gen_porkbun.md b/docs/content/dns/zz_gen_porkbun.md
new file mode 100644
index 0000000000..64a0637ef9
--- /dev/null
+++ b/docs/content/dns/zz_gen_porkbun.md
@@ -0,0 +1,64 @@
+---
+title: "Porkbun"
+date: 2019-03-03T16:39:46+01:00
+draft: false
+slug: porkbun
+---
+
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+<!-- providers/dns/porkbun/porkbun.toml -->
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+
+Since: v4.4.0
+
+Configuration for [Porkbun](https://porkbun.com/).
+
+
+<!--more-->
+
+- Code: `porkbun`
+
+Here is an example bash command using the Porkbun provider:
+
+```bash
+PORKBUN_SECRET_API_KEY=xxxxxx \
+PORKBUN_PAPI_KEY=yyyyyy \
+lego --email myemail@example.com --dns porkbun --domains my.example.org run
+```
+
+
+
+
+## Credentials
+
+| Environment Variable Name | Description |
+|-----------------------|-------------|
+| `PORKBUN_API_KEY` | API key |
+| `PORKBUN_SECRET_API_KEY` | secret API key |
+
+The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
+More information [here](/lego/dns/#configuration-and-credentials).
+
+
+## Additional Configuration
+
+| Environment Variable Name | Description |
+|--------------------------------|-------------|
+| `PORKBUN_HTTP_TIMEOUT` | API request timeout |
+| `PORKBUN_POLLING_INTERVAL` | Time between DNS propagation check |
+| `PORKBUN_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
+| `PORKBUN_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](/lego/dns/#configuration-and-credentials).
+
+
+
+
+## More information
+
+- [API documentation](https://porkbun.com/api/json/v3/documentation)
+
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
+<!-- providers/dns/porkbun/porkbun.toml -->
+<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
diff --git a/go.mod b/go.mod
index 83b8d52f5f..c59a2dc5c6 100644
--- a/go.mod
+++ b/go.mod
@@ -36,6 +36,7 @@ require (
 	github.com/nrdcg/dnspod-go v0.4.0
 	github.com/nrdcg/goinwx v0.8.1
 	github.com/nrdcg/namesilo v0.2.1
+	github.com/nrdcg/porkbun v0.1.1
 	github.com/oracle/oci-go-sdk v24.3.0+incompatible
 	github.com/ovh/go-ovh v1.1.0
 	github.com/pquerna/otp v1.3.0
diff --git a/go.sum b/go.sum
index dbd3b14665..291c7b1d70 100644
--- a/go.sum
+++ b/go.sum
@@ -343,6 +343,8 @@ github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU=
 github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
 github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
 github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
+github.com/nrdcg/porkbun v0.1.1 h1:gxVzQYfFUGXhnBax/aVugoE3OIBAdHgrJgyMPyY5Sjo=
+github.com/nrdcg/porkbun v0.1.1/go.mod h1:JWl/WKnguWos4mjfp4YizvvToigk9qpQwrodOk+CPoA=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go
index bf8ba05c5e..fb6deb3562 100644
--- a/providers/dns/dns_providers.go
+++ b/providers/dns/dns_providers.go
@@ -70,6 +70,7 @@ import (
 	"github.com/go-acme/lego/v4/providers/dns/otc"
 	"github.com/go-acme/lego/v4/providers/dns/ovh"
 	"github.com/go-acme/lego/v4/providers/dns/pdns"
+	"github.com/go-acme/lego/v4/providers/dns/porkbun"
 	"github.com/go-acme/lego/v4/providers/dns/rackspace"
 	"github.com/go-acme/lego/v4/providers/dns/regru"
 	"github.com/go-acme/lego/v4/providers/dns/rfc2136"
@@ -228,6 +229,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
 		return ovh.NewDNSProvider()
 	case "pdns":
 		return pdns.NewDNSProvider()
+	case "porkbun":
+		return porkbun.NewDNSProvider()
 	case "rackspace":
 		return rackspace.NewDNSProvider()
 	case "regru":
@@ -256,10 +259,10 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
 		return vegadns.NewDNSProvider()
 	case "versio":
 		return versio.NewDNSProvider()
-	case "vultr":
-		return vultr.NewDNSProvider()
 	case "vinyldns":
 		return vinyldns.NewDNSProvider()
+	case "vultr":
+		return vultr.NewDNSProvider()
 	case "vscale":
 		return vscale.NewDNSProvider()
 	case "wedos":
diff --git a/providers/dns/porkbun/porkbun.go b/providers/dns/porkbun/porkbun.go
new file mode 100644
index 0000000000..1ec5e52acc
--- /dev/null
+++ b/providers/dns/porkbun/porkbun.go
@@ -0,0 +1,181 @@
+// Package porkbun implements a DNS provider for solving the DNS-01 challenge using Porkbun.
+package porkbun
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/go-acme/lego/v4/challenge/dns01"
+	"github.com/go-acme/lego/v4/platform/config/env"
+	"github.com/nrdcg/porkbun"
+)
+
+// Environment variables names.
+const (
+	envNamespace = "PORKBUN_"
+
+	EnvSecretAPIKey = envNamespace + "SECRET_API_KEY"
+	EnvAPIKey       = envNamespace + "API_KEY"
+
+	EnvTTL                = envNamespace + "TTL"
+	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
+	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
+)
+
+const minTTL = 300
+
+// Config is used to configure the creation of the DNSProvider.
+type Config struct {
+	APIKey             string
+	SecretAPIKey       string
+	PropagationTimeout time.Duration
+	PollingInterval    time.Duration
+	TTL                int
+	HTTPClient         *http.Client
+}
+
+// NewDefaultConfig returns a default configuration for the DNSProvider.
+func NewDefaultConfig() *Config {
+	return &Config{
+		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
+		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
+		TTL:                env.GetOrDefaultInt(EnvTTL, minTTL),
+		HTTPClient: &http.Client{
+			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
+		},
+	}
+}
+
+// DNSProvider implements the challenge.Provider interface.
+type DNSProvider struct {
+	config *Config
+	client *porkbun.Client
+
+	recordIDs   map[string]int
+	recordIDsMu sync.Mutex
+}
+
+// NewDNSProvider returns a DNSProvider instance configured for Porkbun.
+// Credentials must be passed in the environment variables:
+// PORKBUN_SECRET_API_KEY, PORKBUN_PAPI_KEY.
+func NewDNSProvider() (*DNSProvider, error) {
+	values, err := env.Get(EnvSecretAPIKey, EnvAPIKey)
+	if err != nil {
+		return nil, fmt.Errorf("porkbun: %w", err)
+	}
+
+	config := NewDefaultConfig()
+	config.SecretAPIKey = values[EnvSecretAPIKey]
+	config.APIKey = values[EnvAPIKey]
+
+	return NewDNSProviderConfig(config)
+}
+
+// NewDNSProviderConfig return a DNSProvider instance configured for Porkbun.
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+	if config == nil {
+		return nil, errors.New("porkbun: the configuration of the DNS provider is nil")
+	}
+
+	if config.SecretAPIKey == "" || config.APIKey == "" {
+		return nil, errors.New("porkbun: some credentials information are missing")
+	}
+
+	if config.TTL < minTTL {
+		return nil, fmt.Errorf("porkbun: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
+	}
+
+	client := porkbun.New(config.SecretAPIKey, config.APIKey)
+
+	if config.HTTPClient != nil {
+		client.HTTPClient = config.HTTPClient
+	}
+
+	return &DNSProvider{
+		config:    config,
+		client:    client,
+		recordIDs: make(map[string]int),
+	}, 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
+}
+
+// Present creates a TXT record to fulfill the dns-01 challenge.
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+	fqdn, value := dns01.GetRecord(domain, keyAuth)
+
+	zoneName, hostName, err := splitDomain(fqdn)
+	if err != nil {
+		return fmt.Errorf("porkbun: %w", err)
+	}
+
+	record := porkbun.Record{
+		Name:    hostName,
+		Type:    "TXT",
+		Content: value,
+		TTL:     strconv.Itoa(d.config.TTL),
+	}
+
+	ctx := context.Background()
+
+	recordID, err := d.client.CreateRecord(ctx, dns01.UnFqdn(zoneName), record)
+	if err != nil {
+		return fmt.Errorf("porkbun: failed to create record: %w", err)
+	}
+
+	d.recordIDsMu.Lock()
+	d.recordIDs[token] = recordID
+	d.recordIDsMu.Unlock()
+
+	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)
+
+	// gets the record's unique ID from when we created it
+	d.recordIDsMu.Lock()
+	recordID, ok := d.recordIDs[token]
+	d.recordIDsMu.Unlock()
+	if !ok {
+		return fmt.Errorf("porkbun: unknown record ID for '%s' '%s'", fqdn, token)
+	}
+
+	zoneName, _, err := splitDomain(fqdn)
+	if err != nil {
+		return fmt.Errorf("porkbun: %w", err)
+	}
+
+	ctx := context.Background()
+
+	err = d.client.DeleteRecord(ctx, dns01.UnFqdn(zoneName), recordID)
+	if err != nil {
+		return fmt.Errorf("porkbun: failed to delete record: %w", err)
+	}
+
+	return nil
+}
+
+// splitDomain splits the hostname from the authoritative zone, and returns both parts.
+func splitDomain(fqdn string) (string, string, error) {
+	zone, err := dns01.FindZoneByFqdn(fqdn)
+	if err != nil {
+		return "", "", err
+	}
+
+	host := dns01.UnFqdn(strings.TrimSuffix(fqdn, zone))
+
+	return zone, host, nil
+}
diff --git a/providers/dns/porkbun/porkbun.toml b/providers/dns/porkbun/porkbun.toml
new file mode 100644
index 0000000000..ff406148c9
--- /dev/null
+++ b/providers/dns/porkbun/porkbun.toml
@@ -0,0 +1,24 @@
+Name = "Porkbun"
+Description = ''''''
+URL = "https://porkbun.com/"
+Code = "porkbun"
+Since = "v4.4.0"
+
+Example = '''
+PORKBUN_SECRET_API_KEY=xxxxxx \
+PORKBUN_PAPI_KEY=yyyyyy \
+lego --email myemail@example.com --dns porkbun --domains my.example.org run
+'''
+
+[Configuration]
+  [Configuration.Credentials]
+    PORKBUN_SECRET_API_KEY = "secret API key"
+    PORKBUN_API_KEY = "API key"
+  [Configuration.Additional]
+    PORKBUN_POLLING_INTERVAL = "Time between DNS propagation check"
+    PORKBUN_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
+    PORKBUN_TTL = "The TTL of the TXT record used for the DNS challenge"
+    PORKBUN_HTTP_TIMEOUT = "API request timeout"
+
+[Links]
+  API = "https://porkbun.com/api/json/v3/documentation"


Test patch:
diff --git a/providers/dns/checkdomain/client_test.go b/providers/dns/checkdomain/client_test.go
index 3d470241ff..efc75101f6 100644
--- a/providers/dns/checkdomain/client_test.go
+++ b/providers/dns/checkdomain/client_test.go
@@ -39,6 +39,7 @@ func Test_getDomainIDByName(t *testing.T) {
 	handler.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) {
 		if req.Method != http.MethodGet {
 			http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
+			return
 		}
 
 		domainList := DomainListingResponse{
@@ -67,6 +68,7 @@ func Test_checkNameservers(t *testing.T) {
 	handler.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) {
 		if req.Method != http.MethodGet {
 			http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
+			return
 		}
 
 		nsResp := NameserverResponse{
@@ -94,6 +96,7 @@ func Test_createRecord(t *testing.T) {
 	handler.HandleFunc("/v1/domains/1/nameservers/records", func(rw http.ResponseWriter, req *http.Request) {
 		if req.Method != http.MethodPost {
 			http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
+			return
 		}
 
 		content, err := ioutil.ReadAll(req.Body)
@@ -172,6 +175,7 @@ func Test_deleteTXTRecord(t *testing.T) {
 	handler.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) {
 		if req.Method != http.MethodGet {
 			http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
+			return
 		}
 
 		nsResp := NameserverResponse{
diff --git a/providers/dns/porkbun/porkbun_test.go b/providers/dns/porkbun/porkbun_test.go
new file mode 100644
index 0000000000..9bc86adc1a
--- /dev/null
+++ b/providers/dns/porkbun/porkbun_test.go
@@ -0,0 +1,150 @@
+package porkbun
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/stretchr/testify/require"
+)
+
+const envDomain = envNamespace + "DOMAIN"
+
+var envTest = tester.NewEnvTest(EnvSecretAPIKey, EnvAPIKey).
+	WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		envVars  map[string]string
+		expected string
+	}{
+		{
+			desc: "success",
+			envVars: map[string]string{
+				EnvSecretAPIKey: "secret",
+				EnvAPIKey:       "key",
+			},
+		},
+		{
+			desc: "missing secret API key",
+			envVars: map[string]string{
+				EnvSecretAPIKey: "",
+				EnvAPIKey:       "key",
+			},
+			expected: "porkbun: some credentials information are missing: PORKBUN_SECRET_API_KEY",
+		},
+		{
+			desc: "missing API key",
+			envVars: map[string]string{
+				EnvSecretAPIKey: "secret",
+				EnvAPIKey:       "",
+			},
+			expected: "porkbun: some credentials information are missing: PORKBUN_API_KEY",
+		},
+		{
+			desc: "missing all credentials",
+			envVars: map[string]string{
+				EnvSecretAPIKey: "",
+				EnvAPIKey:       "",
+			},
+			expected: "porkbun: some credentials information are missing: PORKBUN_SECRET_API_KEY,PORKBUN_API_KEY",
+		},
+	}
+
+	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
+		secretAPIKey string
+		apiKey       string
+		expected     string
+	}{
+		{
+			desc:         "success",
+			secretAPIKey: "secret",
+			apiKey:       "key",
+		},
+		{
+			desc:     "missing secret API key",
+			apiKey:   "key",
+			expected: "porkbun: some credentials information are missing",
+		},
+		{
+			desc:         "missing API key",
+			secretAPIKey: "secret",
+			expected:     "porkbun: some credentials information are missing",
+		},
+		{
+			desc:     "missing all credentials",
+			expected: "porkbun: some credentials information are missing",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			config := NewDefaultConfig()
+			config.SecretAPIKey = test.secretAPIKey
+			config.APIKey = test.apiKey
+
+			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 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)
+}
+
+func TestName(t *testing.T) {
+	fmt.Println(splitDomain("_acme-challenge.example.com."))
+}