forked from TrueCloudLab/lego
cloudns: Improve reliability (#1331)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
parent
b65dfb8661
commit
8b8be6f21e
4 changed files with 636 additions and 172 deletions
|
@ -8,7 +8,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||||
|
"github.com/go-acme/lego/v4/log"
|
||||||
"github.com/go-acme/lego/v4/platform/config/env"
|
"github.com/go-acme/lego/v4/platform/config/env"
|
||||||
|
"github.com/go-acme/lego/v4/platform/wait"
|
||||||
"github.com/go-acme/lego/v4/providers/dns/cloudns/internal"
|
"github.com/go-acme/lego/v4/providers/dns/cloudns/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,8 +43,8 @@ type Config struct {
|
||||||
func NewDefaultConfig() *Config {
|
func NewDefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
TTL: env.GetOrDefaultInt(EnvTTL, 60),
|
TTL: env.GetOrDefaultInt(EnvTTL, 60),
|
||||||
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),
|
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 180*time.Second),
|
||||||
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),
|
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
|
||||||
HTTPClient: &http.Client{
|
HTTPClient: &http.Client{
|
||||||
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
|
||||||
},
|
},
|
||||||
|
@ -112,10 +114,10 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
return fmt.Errorf("ClouDNS: %w", err)
|
return fmt.Errorf("ClouDNS: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return d.waitNameservers(domain, zone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanUp removes the TXT record matching the specified parameters.
|
// CleanUp removes the TXT records matching the specified parameters.
|
||||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
fqdn, _ := dns01.GetRecord(domain, keyAuth)
|
||||||
|
|
||||||
|
@ -124,19 +126,22 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
return fmt.Errorf("ClouDNS: %w", err)
|
return fmt.Errorf("ClouDNS: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
record, err := d.client.FindTxtRecord(zone.Name, fqdn)
|
records, err := d.client.ListTxtRecords(zone.Name, fqdn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ClouDNS: %w", err)
|
return fmt.Errorf("ClouDNS: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if record == nil {
|
if len(records) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.client.RemoveTxtRecord(record.ID, zone.Name)
|
for _, record := range records {
|
||||||
if err != nil {
|
err = d.client.RemoveTxtRecord(record.ID, zone.Name)
|
||||||
return fmt.Errorf("ClouDNS: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("ClouDNS: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,3 +150,18 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
return d.config.PropagationTimeout, d.config.PollingInterval
|
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitNameservers At the time of writing 4 servers are found as authoritative, but 8 are reported during the sync.
|
||||||
|
// If this is not done, the secondary verification done by Let's Encrypt server will fail quire a bit.
|
||||||
|
func (d *DNSProvider) waitNameservers(domain string, zone *internal.Zone) error {
|
||||||
|
return wait.For("Nameserver sync on "+domain, d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
|
||||||
|
syncProgress, err := d.client.GetUpdateStatus(zone.Name)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[%s] Sync %d/%d complete", domain, syncProgress.Updated, syncProgress.Total)
|
||||||
|
|
||||||
|
return syncProgress.Complete, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -15,31 +16,15 @@ import (
|
||||||
|
|
||||||
const defaultBaseURL = "https://api.cloudns.net/dns/"
|
const defaultBaseURL = "https://api.cloudns.net/dns/"
|
||||||
|
|
||||||
type apiResponse struct {
|
// Client the ClouDNS client.
|
||||||
Status string `json:"status"`
|
type Client struct {
|
||||||
StatusDescription string `json:"statusDescription"`
|
authID string
|
||||||
|
subAuthID string
|
||||||
|
authPassword string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
BaseURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
type Zone struct {
|
|
||||||
Name string
|
|
||||||
Type string
|
|
||||||
Zone string
|
|
||||||
Status string // is an integer, but cast as string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TXTRecord a TXT record.
|
|
||||||
type TXTRecord struct {
|
|
||||||
ID int `json:"id,string"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Record string `json:"record"`
|
|
||||||
Failover int `json:"failover,string"`
|
|
||||||
TTL int `json:"ttl,string"`
|
|
||||||
Status int `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TXTRecords map[string]TXTRecord
|
|
||||||
|
|
||||||
// NewClient creates a ClouDNS client.
|
// NewClient creates a ClouDNS client.
|
||||||
func NewClient(authID, subAuthID, authPassword string) (*Client, error) {
|
func NewClient(authID, subAuthID, authPassword string) (*Client, error) {
|
||||||
if authID == "" && subAuthID == "" {
|
if authID == "" && subAuthID == "" {
|
||||||
|
@ -64,15 +49,6 @@ func NewClient(authID, subAuthID, authPassword string) (*Client, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client ClouDNS client.
|
|
||||||
type Client struct {
|
|
||||||
authID string
|
|
||||||
subAuthID string
|
|
||||||
authPassword string
|
|
||||||
HTTPClient *http.Client
|
|
||||||
BaseURL *url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetZone Get domain name information for a FQDN.
|
// GetZone Get domain name information for a FQDN.
|
||||||
func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
||||||
authZone, err := dns01.FindZoneByFqdn(authFQDN)
|
authZone, err := dns01.FindZoneByFqdn(authFQDN)
|
||||||
|
@ -82,14 +58,16 @@ func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
||||||
|
|
||||||
authZoneName := dns01.UnFqdn(authZone)
|
authZoneName := dns01.UnFqdn(authZone)
|
||||||
|
|
||||||
reqURL := *c.BaseURL
|
endpoint, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "get-zone-info.json"))
|
||||||
reqURL.Path += "get-zone-info.json"
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
q := reqURL.Query()
|
q := endpoint.Query()
|
||||||
q.Add("domain-name", authZoneName)
|
q.Set("domain-name", authZoneName)
|
||||||
reqURL.RawQuery = q.Encode()
|
endpoint.RawQuery = q.Encode()
|
||||||
|
|
||||||
result, err := c.doRequest(http.MethodGet, &reqURL)
|
result, err := c.doRequest(http.MethodGet, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -98,7 +76,7 @@ func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
||||||
|
|
||||||
if len(result) > 0 {
|
if len(result) > 0 {
|
||||||
if err = json.Unmarshal(result, &zone); err != nil {
|
if err = json.Unmarshal(result, &zone); err != nil {
|
||||||
return nil, fmt.Errorf("zone unmarshaling error: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal zone: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,20 +87,22 @@ func (c *Client) GetZone(authFQDN string) (*Zone, error) {
|
||||||
return nil, fmt.Errorf("zone %s not found for authFQDN %s", authZoneName, authFQDN)
|
return nil, fmt.Errorf("zone %s not found for authFQDN %s", authZoneName, authFQDN)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindTxtRecord return the TXT record a zone ID and a FQDN.
|
// FindTxtRecord returns the TXT record a zone ID and a FQDN.
|
||||||
func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
|
func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
|
||||||
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
||||||
|
|
||||||
reqURL := *c.BaseURL
|
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "records.json"))
|
||||||
reqURL.Path += "records.json"
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
q := reqURL.Query()
|
q := reqURL.Query()
|
||||||
q.Add("domain-name", zoneName)
|
q.Set("domain-name", zoneName)
|
||||||
q.Add("host", host)
|
q.Set("host", host)
|
||||||
q.Add("type", "TXT")
|
q.Set("type", "TXT")
|
||||||
reqURL.RawQuery = q.Encode()
|
reqURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
result, err := c.doRequest(http.MethodGet, &reqURL)
|
result, err := c.doRequest(http.MethodGet, reqURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -132,9 +112,9 @@ func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var records TXTRecords
|
var records map[string]TXTRecord
|
||||||
if err = json.Unmarshal(result, &records); err != nil {
|
if err = json.Unmarshal(result, &records); err != nil {
|
||||||
return nil, fmt.Errorf("TXT record unmarshaling error: %w: %s", err, string(result))
|
return nil, fmt.Errorf("failed to unmarshall TXT records: %w: %s", err, string(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
|
@ -146,65 +126,145 @@ func (c *Client) FindTxtRecord(zoneName, fqdn string) (*TXTRecord, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTxtRecord add a TXT record.
|
// ListTxtRecords returns the TXT records a zone ID and a FQDN.
|
||||||
|
func (c *Client) ListTxtRecords(zoneName, fqdn string) ([]TXTRecord, error) {
|
||||||
|
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
||||||
|
|
||||||
|
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "records.json"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := reqURL.Query()
|
||||||
|
q.Set("domain-name", zoneName)
|
||||||
|
q.Set("host", host)
|
||||||
|
q.Set("type", "TXT")
|
||||||
|
reqURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
result, err := c.doRequest(http.MethodGet, reqURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// the API returns [] when there is no records.
|
||||||
|
if string(result) == "[]" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw map[string]TXTRecord
|
||||||
|
if err = json.Unmarshal(result, &raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshall TXT records: %w: %s", err, string(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []TXTRecord
|
||||||
|
for _, record := range raw {
|
||||||
|
if record.Host == host && record.Type == "TXT" {
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTxtRecord adds a TXT record.
|
||||||
func (c *Client) AddTxtRecord(zoneName, fqdn, value string, ttl int) error {
|
func (c *Client) AddTxtRecord(zoneName, fqdn, value string, ttl int) error {
|
||||||
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
host := dns01.UnFqdn(strings.TrimSuffix(dns01.UnFqdn(fqdn), zoneName))
|
||||||
|
|
||||||
reqURL := *c.BaseURL
|
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "add-record.json"))
|
||||||
reqURL.Path += "add-record.json"
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
q := reqURL.Query()
|
q := reqURL.Query()
|
||||||
q.Add("domain-name", zoneName)
|
q.Set("domain-name", zoneName)
|
||||||
q.Add("host", host)
|
q.Set("host", host)
|
||||||
q.Add("record", value)
|
q.Set("record", value)
|
||||||
q.Add("ttl", strconv.Itoa(ttlRounder(ttl)))
|
q.Set("ttl", strconv.Itoa(ttlRounder(ttl)))
|
||||||
q.Add("record-type", "TXT")
|
q.Set("record-type", "TXT")
|
||||||
reqURL.RawQuery = q.Encode()
|
reqURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
raw, err := c.doRequest(http.MethodPost, &reqURL)
|
raw, err := c.doRequest(http.MethodPost, reqURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := apiResponse{}
|
resp := apiResponse{}
|
||||||
if err = json.Unmarshal(raw, &resp); err != nil {
|
if err = json.Unmarshal(raw, &resp); err != nil {
|
||||||
return fmt.Errorf("apiResponse unmarshaling error: %w: %s", err, string(raw))
|
return fmt.Errorf("failed to unmarshal API response: %w: %s", err, string(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Status != "Success" {
|
if resp.Status != "Success" {
|
||||||
return fmt.Errorf("fail to add TXT record: %s %s", resp.Status, resp.StatusDescription)
|
return fmt.Errorf("failed to add TXT record: %s %s", resp.Status, resp.StatusDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveTxtRecord remove a TXT record.
|
// RemoveTxtRecord removes a TXT record.
|
||||||
func (c *Client) RemoveTxtRecord(recordID int, zoneName string) error {
|
func (c *Client) RemoveTxtRecord(recordID int, zoneName string) error {
|
||||||
reqURL := *c.BaseURL
|
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "delete-record.json"))
|
||||||
reqURL.Path += "delete-record.json"
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
q := reqURL.Query()
|
q := reqURL.Query()
|
||||||
q.Add("domain-name", zoneName)
|
q.Set("domain-name", zoneName)
|
||||||
q.Add("record-id", strconv.Itoa(recordID))
|
q.Set("record-id", strconv.Itoa(recordID))
|
||||||
reqURL.RawQuery = q.Encode()
|
reqURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
raw, err := c.doRequest(http.MethodPost, &reqURL)
|
raw, err := c.doRequest(http.MethodPost, reqURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := apiResponse{}
|
resp := apiResponse{}
|
||||||
if err = json.Unmarshal(raw, &resp); err != nil {
|
if err = json.Unmarshal(raw, &resp); err != nil {
|
||||||
return fmt.Errorf("apiResponse unmarshaling error: %w: %s", err, string(raw))
|
return fmt.Errorf("failed to unmarshal API response: %w: %s", err, string(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Status != "Success" {
|
if resp.Status != "Success" {
|
||||||
return fmt.Errorf("fail to add TXT record: %s %s", resp.Status, resp.StatusDescription)
|
return fmt.Errorf("failed to remove TXT record: %s %s", resp.Status, resp.StatusDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUpdateStatus gets sync progress of all CloudDNS NS servers.
|
||||||
|
func (c *Client) GetUpdateStatus(zoneName string) (*SyncProgress, error) {
|
||||||
|
reqURL, err := c.BaseURL.Parse(path.Join(c.BaseURL.Path, "update-status.json"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := reqURL.Query()
|
||||||
|
q.Set("domain-name", zoneName)
|
||||||
|
reqURL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
result, err := c.doRequest(http.MethodGet, reqURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// the API returns [] when there is no records.
|
||||||
|
if string(result) == "[]" {
|
||||||
|
return nil, errors.New("no nameservers records returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []UpdateRecord
|
||||||
|
if err = json.Unmarshal(result, &records); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal UpdateRecord: %w: %s", err, string(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedCount := 0
|
||||||
|
for _, record := range records {
|
||||||
|
if record.Updated {
|
||||||
|
updatedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SyncProgress{Complete: updatedCount == len(records), Updated: updatedCount, Total: len(records)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error) {
|
func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error) {
|
||||||
req, err := c.buildRequest(method, url)
|
req, err := c.buildRequest(method, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -224,8 +284,9 @@ func (c *Client) doRequest(method string, url *url.URL) (json.RawMessage, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return nil, fmt.Errorf("invalid code (%v), error: %s", resp.StatusCode, content)
|
return nil, fmt.Errorf("invalid code (%d), error: %s", resp.StatusCode, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,12 +294,12 @@ func (c *Client) buildRequest(method string, url *url.URL) (*http.Request, error
|
||||||
q := url.Query()
|
q := url.Query()
|
||||||
|
|
||||||
if c.subAuthID != "" {
|
if c.subAuthID != "" {
|
||||||
q.Add("sub-auth-id", c.subAuthID)
|
q.Set("sub-auth-id", c.subAuthID)
|
||||||
} else {
|
} else {
|
||||||
q.Add("auth-id", c.authID)
|
q.Set("auth-id", c.authID)
|
||||||
}
|
}
|
||||||
|
|
||||||
q.Add("auth-password", c.authPassword)
|
q.Set("auth-password", c.authPassword)
|
||||||
|
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
@ -269,7 +330,6 @@ func toUnreadableBodyMessage(req *http.Request, rawBody []byte) string {
|
||||||
// - 604800 = 1 week
|
// - 604800 = 1 week
|
||||||
// - 1209600 = 2 weeks
|
// - 1209600 = 2 weeks
|
||||||
// - 2592000 = 1 month
|
// - 2592000 = 1 month
|
||||||
// - 2592000 = 1 month
|
|
||||||
// See https://www.cloudns.net/wiki/article/58/ for details.
|
// See https://www.cloudns.net/wiki/article/58/ for details.
|
||||||
func ttlRounder(ttl int) int {
|
func ttlRounder(ttl int) int {
|
||||||
for _, validTTL := range []int{60, 300, 900, 1800, 3600, 21600, 43200, 86400, 172800, 259200, 604800, 1209600} {
|
for _, validTTL := range []int{60, 300, 900, 1800, 3600, 21600, 43200, 86400, 172800, 259200, 604800, 1209600} {
|
||||||
|
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerMock(method string, jsonData []byte) http.Handler {
|
func handlerMock(method string, jsonData []byte) http.HandlerFunc {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
return func(rw http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != method {
|
if req.Method != method {
|
||||||
http.Error(rw, "Incorrect method used", http.StatusBadRequest)
|
http.Error(rw, "Incorrect method used", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -23,25 +23,71 @@ func handlerMock(method string, jsonData []byte) http.Handler {
|
||||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientGetZone(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
type result struct {
|
testCases := []struct {
|
||||||
zone *Zone
|
desc string
|
||||||
error bool
|
authID string
|
||||||
|
subAuthID string
|
||||||
|
authPassword string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "all provided",
|
||||||
|
authID: "1000",
|
||||||
|
subAuthID: "1111",
|
||||||
|
authPassword: "no-secret",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing authID & subAuthID",
|
||||||
|
authID: "",
|
||||||
|
subAuthID: "",
|
||||||
|
authPassword: "no-secret",
|
||||||
|
expected: "credentials missing: authID or subAuthID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing authID & subAuthID",
|
||||||
|
authID: "",
|
||||||
|
subAuthID: "present",
|
||||||
|
authPassword: "",
|
||||||
|
expected: "credentials missing: authPassword",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
client, err := NewClient(test.authID, test.subAuthID, test.authPassword)
|
||||||
|
|
||||||
|
if test.expected != "" {
|
||||||
|
assert.Nil(t, client)
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, client)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetZone(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
zone *Zone
|
||||||
|
errorMsg string
|
||||||
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
authFQDN string
|
authFQDN string
|
||||||
apiResponse []byte
|
apiResponse string
|
||||||
expected result
|
expected
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "zone found",
|
desc: "zone found",
|
||||||
authFQDN: "_acme-challenge.foo.com.",
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
apiResponse: []byte(`{"name": "foo.com", "type": "master", "zone": "zone", "status": "1"}`),
|
apiResponse: `{"name": "foo.com", "type": "master", "zone": "zone", "status": "1"}`,
|
||||||
expected: result{
|
expected: expected{
|
||||||
zone: &Zone{
|
zone: &Zone{
|
||||||
Name: "foo.com",
|
Name: "foo.com",
|
||||||
Type: "master",
|
Type: "master",
|
||||||
|
@ -53,23 +99,35 @@ func TestClientGetZone(t *testing.T) {
|
||||||
{
|
{
|
||||||
desc: "zone not found",
|
desc: "zone not found",
|
||||||
authFQDN: "_acme-challenge.foo.com.",
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
apiResponse: []byte(``),
|
apiResponse: ``,
|
||||||
expected: result{error: true},
|
expected: expected{
|
||||||
|
errorMsg: "zone foo.com not found for authFQDN _acme-challenge.foo.com.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid json response",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
apiResponse: `[{}]`,
|
||||||
|
expected: expected{
|
||||||
|
errorMsg: "failed to unmarshal zone: json: cannot unmarshal array into Go value of type internal.Zone",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse))
|
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
client, _ := NewClient("myAuthID", "", "myAuthPassword")
|
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||||
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
|
require.NoError(t, err)
|
||||||
client.BaseURL = mockBaseURL
|
|
||||||
|
client.BaseURL, _ = url.Parse(server.URL)
|
||||||
|
|
||||||
zone, err := client.GetZone(test.authFQDN)
|
zone, err := client.GetZone(test.authFQDN)
|
||||||
|
|
||||||
if test.expected.error {
|
if test.expected.errorMsg != "" {
|
||||||
require.Error(t, err)
|
require.EqualError(t, err, test.expected.errorMsg)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, test.expected.zone, zone)
|
assert.Equal(t, test.expected.zone, zone)
|
||||||
|
@ -78,24 +136,24 @@ func TestClientGetZone(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientFindTxtRecord(t *testing.T) {
|
func TestClient_FindTxtRecord(t *testing.T) {
|
||||||
type result struct {
|
type expected struct {
|
||||||
txtRecord *TXTRecord
|
txtRecord *TXTRecord
|
||||||
error bool
|
errorMsg string
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
authFQDN string
|
authFQDN string
|
||||||
zoneName string
|
zoneName string
|
||||||
apiResponse []byte
|
apiResponse string
|
||||||
expected result
|
expected
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "record found",
|
desc: "record found",
|
||||||
authFQDN: "_acme-challenge.foo.com.",
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
zoneName: "foo.com",
|
zoneName: "foo.com",
|
||||||
apiResponse: []byte(`{
|
apiResponse: `{
|
||||||
"5769228": {
|
"5769228": {
|
||||||
"id": "5769228",
|
"id": "5769228",
|
||||||
"type": "TXT",
|
"type": "TXT",
|
||||||
|
@ -114,8 +172,8 @@ func TestClientFindTxtRecord(t *testing.T) {
|
||||||
"ttl": "300",
|
"ttl": "300",
|
||||||
"status": 1
|
"status": 1
|
||||||
}
|
}
|
||||||
}`),
|
}`,
|
||||||
expected: result{
|
expected: expected{
|
||||||
txtRecord: &TXTRecord{
|
txtRecord: &TXTRecord{
|
||||||
ID: 5769228,
|
ID: 5769228,
|
||||||
Type: "TXT",
|
Type: "TXT",
|
||||||
|
@ -128,28 +186,61 @@ func TestClientFindTxtRecord(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "record not found",
|
desc: "no record found",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "foo.com",
|
||||||
|
apiResponse: `{
|
||||||
|
"5769228": {
|
||||||
|
"id": "5769228",
|
||||||
|
"type": "TXT",
|
||||||
|
"host": "_other-challenge",
|
||||||
|
"record": "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||||
|
"failover": "0",
|
||||||
|
"ttl": "3600",
|
||||||
|
"status": 1
|
||||||
|
},
|
||||||
|
"181805209": {
|
||||||
|
"id": "181805209",
|
||||||
|
"type": "TXT",
|
||||||
|
"host": "_github-challenge",
|
||||||
|
"record": "b66b8324b5",
|
||||||
|
"failover": "0",
|
||||||
|
"ttl": "300",
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "zero records",
|
||||||
authFQDN: "_acme-challenge.foo.com.",
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
zoneName: "test-zone",
|
zoneName: "test-zone",
|
||||||
apiResponse: []byte(`[]`),
|
apiResponse: `[]`,
|
||||||
expected: result{txtRecord: nil},
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid json response",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "test-zone",
|
||||||
|
apiResponse: `[{}]`,
|
||||||
|
expected: expected{
|
||||||
|
errorMsg: "failed to unmarshall TXT records: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord: [{}]",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
server := httptest.NewServer(handlerMock(http.MethodGet, test.apiResponse))
|
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
|
client.BaseURL, _ = url.Parse(server.URL)
|
||||||
client.BaseURL = mockBaseURL
|
|
||||||
|
|
||||||
txtRecord, err := client.FindTxtRecord(test.zoneName, test.authFQDN)
|
txtRecord, err := client.FindTxtRecord(test.zoneName, test.authFQDN)
|
||||||
|
|
||||||
if test.expected.error {
|
if test.expected.errorMsg != "" {
|
||||||
require.Error(t, err)
|
require.EqualError(t, err, test.expected.errorMsg)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, test.expected.txtRecord, txtRecord)
|
assert.Equal(t, test.expected.txtRecord, txtRecord)
|
||||||
|
@ -158,91 +249,198 @@ func TestClientFindTxtRecord(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientAddTxtRecord(t *testing.T) {
|
func TestClient_ListTxtRecord(t *testing.T) {
|
||||||
type expected struct {
|
type expected struct {
|
||||||
Query string
|
txtRecords []TXTRecord
|
||||||
Error string
|
errorMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
authFQDN string
|
||||||
|
zoneName string
|
||||||
|
apiResponse string
|
||||||
|
expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "record found",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "foo.com",
|
||||||
|
apiResponse: `{
|
||||||
|
"5769228": {
|
||||||
|
"id": "5769228",
|
||||||
|
"type": "TXT",
|
||||||
|
"host": "_acme-challenge",
|
||||||
|
"record": "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||||
|
"failover": "0",
|
||||||
|
"ttl": "3600",
|
||||||
|
"status": 1
|
||||||
|
},
|
||||||
|
"181805209": {
|
||||||
|
"id": "181805209",
|
||||||
|
"type": "TXT",
|
||||||
|
"host": "_github-challenge",
|
||||||
|
"record": "b66b8324b5",
|
||||||
|
"failover": "0",
|
||||||
|
"ttl": "300",
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: expected{
|
||||||
|
txtRecords: []TXTRecord{
|
||||||
|
{
|
||||||
|
ID: 5769228,
|
||||||
|
Type: "TXT",
|
||||||
|
Host: "_acme-challenge",
|
||||||
|
Record: "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||||
|
Failover: 0,
|
||||||
|
TTL: 3600,
|
||||||
|
Status: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "no record found",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "foo.com",
|
||||||
|
apiResponse: `{
|
||||||
|
"5769228": {
|
||||||
|
"id": "5769228",
|
||||||
|
"type": "TXT",
|
||||||
|
"host": "_other-challenge",
|
||||||
|
"record": "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||||
|
"failover": "0",
|
||||||
|
"ttl": "3600",
|
||||||
|
"status": 1
|
||||||
|
},
|
||||||
|
"181805209": {
|
||||||
|
"id": "181805209",
|
||||||
|
"type": "TXT",
|
||||||
|
"host": "_github-challenge",
|
||||||
|
"record": "b66b8324b5",
|
||||||
|
"failover": "0",
|
||||||
|
"ttl": "300",
|
||||||
|
"status": 1
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "zero records",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "test-zone",
|
||||||
|
apiResponse: `[]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid json response",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "test-zone",
|
||||||
|
apiResponse: `[{}]`,
|
||||||
|
expected: expected{
|
||||||
|
errorMsg: "failed to unmarshall TXT records: json: cannot unmarshal array into Go value of type map[string]internal.TXTRecord: [{}]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.BaseURL, _ = url.Parse(server.URL)
|
||||||
|
|
||||||
|
txtRecords, err := client.ListTxtRecords(test.zoneName, test.authFQDN)
|
||||||
|
|
||||||
|
if test.expected.errorMsg != "" {
|
||||||
|
require.EqualError(t, err, test.expected.errorMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected.txtRecords, txtRecords)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AddTxtRecord(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
query string
|
||||||
|
errorMsg string
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
authID string
|
authID string
|
||||||
subAuthID string
|
subAuthID string
|
||||||
zone *Zone
|
zoneName string
|
||||||
authFQDN string
|
authFQDN string
|
||||||
value string
|
value string
|
||||||
ttl int
|
ttl int
|
||||||
apiResponse []byte
|
apiResponse string
|
||||||
expected expected
|
expected
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
desc: "sub-zone",
|
desc: "sub-zone",
|
||||||
authID: "myAuthID",
|
authID: "myAuthID",
|
||||||
zone: &Zone{
|
zoneName: "bar.com",
|
||||||
Name: "bar.com",
|
|
||||||
Type: "master",
|
|
||||||
Zone: "domain",
|
|
||||||
Status: "1",
|
|
||||||
},
|
|
||||||
authFQDN: "_acme-challenge.foo.bar.com.",
|
authFQDN: "_acme-challenge.foo.bar.com.",
|
||||||
value: "txtTXTtxtTXTtxtTXTtxtTXT",
|
value: "txtTXTtxtTXTtxtTXTtxtTXT",
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
apiResponse: []byte(`{"status":"Success","statusDescription":"The record was added successfully."}`),
|
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
|
||||||
expected: expected{
|
expected: expected{
|
||||||
Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`,
|
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "main zone (authID)",
|
desc: "main zone (authID)",
|
||||||
authID: "myAuthID",
|
authID: "myAuthID",
|
||||||
zone: &Zone{
|
zoneName: "bar.com",
|
||||||
Name: "bar.com",
|
|
||||||
Type: "master",
|
|
||||||
Zone: "domain",
|
|
||||||
Status: "1",
|
|
||||||
},
|
|
||||||
authFQDN: "_acme-challenge.bar.com.",
|
authFQDN: "_acme-challenge.bar.com.",
|
||||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
apiResponse: []byte(`{"status":"Success","statusDescription":"The record was added successfully."}`),
|
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
|
||||||
expected: expected{
|
expected: expected{
|
||||||
Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`,
|
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "main zone (subAuthID)",
|
desc: "main zone (subAuthID)",
|
||||||
authID: "myAuthID",
|
subAuthID: "mySubAuthID",
|
||||||
subAuthID: "mySubAuthID",
|
zoneName: "bar.com",
|
||||||
zone: &Zone{
|
|
||||||
Name: "bar.com",
|
|
||||||
Type: "master",
|
|
||||||
Zone: "domain",
|
|
||||||
Status: "1",
|
|
||||||
},
|
|
||||||
authFQDN: "_acme-challenge.bar.com.",
|
authFQDN: "_acme-challenge.bar.com.",
|
||||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||||
ttl: 60,
|
ttl: 60,
|
||||||
apiResponse: []byte(`{"status":"Success","statusDescription":"The record was added successfully."}`),
|
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
|
||||||
expected: expected{
|
expected: expected{
|
||||||
Query: `auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`,
|
query: `auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "invalid status",
|
desc: "invalid status",
|
||||||
authID: "myAuthID",
|
authID: "myAuthID",
|
||||||
zone: &Zone{
|
zoneName: "bar.com",
|
||||||
Name: "bar.com",
|
|
||||||
Type: "master",
|
|
||||||
Zone: "domain",
|
|
||||||
Status: "1",
|
|
||||||
},
|
|
||||||
authFQDN: "_acme-challenge.bar.com.",
|
authFQDN: "_acme-challenge.bar.com.",
|
||||||
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||||
ttl: 120,
|
ttl: 120,
|
||||||
apiResponse: []byte(`{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`),
|
apiResponse: `{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`,
|
||||||
expected: expected{
|
expected: expected{
|
||||||
Query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
|
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
|
||||||
Error: "fail to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.",
|
errorMsg: "failed to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid json response",
|
||||||
|
authID: "myAuthID",
|
||||||
|
zoneName: "bar.com",
|
||||||
|
authFQDN: "_acme-challenge.bar.com.",
|
||||||
|
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
|
||||||
|
ttl: 120,
|
||||||
|
apiResponse: `[{}]`,
|
||||||
|
expected: expected{
|
||||||
|
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=bar.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
|
||||||
|
errorMsg: "failed to unmarshal API response: json: cannot unmarshal array into Go value of type internal.apiResponse: [{}]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -250,25 +448,172 @@ func TestClientAddTxtRecord(t *testing.T) {
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
assert.NotNil(t, req.URL.RawQuery)
|
if test.expected.query != req.URL.RawQuery {
|
||||||
assert.Equal(t, test.expected.Query, req.URL.RawQuery)
|
msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery)
|
||||||
|
http.Error(rw, msg, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handlerMock(http.MethodPost, test.apiResponse).ServeHTTP(rw, req)
|
handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
client, err := NewClient(test.authID, test.subAuthID, "myAuthPassword")
|
client, err := NewClient(test.authID, test.subAuthID, "myAuthPassword")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
mockBaseURL, _ := url.Parse(fmt.Sprintf("%s/", server.URL))
|
client.BaseURL, _ = url.Parse(server.URL)
|
||||||
client.BaseURL = mockBaseURL
|
|
||||||
|
|
||||||
err = client.AddTxtRecord(test.zone.Name, test.authFQDN, test.value, test.ttl)
|
err = client.AddTxtRecord(test.zoneName, test.authFQDN, test.value, test.ttl)
|
||||||
|
|
||||||
if test.expected.Error != "" {
|
if test.expected.errorMsg != "" {
|
||||||
require.EqualError(t, err, test.expected.Error)
|
require.EqualError(t, err, test.expected.errorMsg)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClient_RemoveTxtRecord(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
query string
|
||||||
|
errorMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
id int
|
||||||
|
zoneName string
|
||||||
|
apiResponse string
|
||||||
|
expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "record found",
|
||||||
|
id: 5769228,
|
||||||
|
zoneName: "foo.com",
|
||||||
|
apiResponse: `{ "status": "Success", "statusDescription": "The record was deleted successfully." }`,
|
||||||
|
expected: expected{
|
||||||
|
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769228`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "record not found",
|
||||||
|
id: 5769000,
|
||||||
|
zoneName: "foo.com",
|
||||||
|
apiResponse: `{ "status": "Failed", "statusDescription": "Invalid record-id param." }`,
|
||||||
|
expected: expected{
|
||||||
|
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769000`,
|
||||||
|
errorMsg: "failed to remove TXT record: Failed Invalid record-id param.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid json response",
|
||||||
|
id: 44,
|
||||||
|
zoneName: "foo-plus.com",
|
||||||
|
apiResponse: `[{}]`,
|
||||||
|
expected: expected{
|
||||||
|
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo-plus.com&record-id=44`,
|
||||||
|
errorMsg: "failed to unmarshal API response: json: cannot unmarshal array into Go value of type internal.apiResponse: [{}]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if test.expected.query != req.URL.RawQuery {
|
||||||
|
msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery)
|
||||||
|
http.Error(rw, msg, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req)
|
||||||
|
}))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.BaseURL, _ = url.Parse(server.URL)
|
||||||
|
|
||||||
|
err = client.RemoveTxtRecord(test.id, test.zoneName)
|
||||||
|
|
||||||
|
if test.expected.errorMsg != "" {
|
||||||
|
require.EqualError(t, err, test.expected.errorMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_GetUpdateStatus(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
progress *SyncProgress
|
||||||
|
errorMsg string
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
authFQDN string
|
||||||
|
zoneName string
|
||||||
|
apiResponse string
|
||||||
|
expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "50% sync",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "foo.com",
|
||||||
|
apiResponse: `[
|
||||||
|
{"server": "ns101.foo.com.", "ip4": "10.11.12.13", "ip6": "2a00:2a00:2a00:9::5", "updated": true },
|
||||||
|
{"server": "ns102.foo.com.", "ip4": "10.14.16.17", "ip6": "2100:2100:2100:3::1", "updated": false }
|
||||||
|
]`,
|
||||||
|
expected: expected{progress: &SyncProgress{Updated: 1, Total: 2}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "100% sync",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "foo.com",
|
||||||
|
apiResponse: `[
|
||||||
|
{"server": "ns101.foo.com.", "ip4": "10.11.12.13", "ip6": "2a00:2a00:2a00:9::5", "updated": true },
|
||||||
|
{"server": "ns102.foo.com.", "ip4": "10.14.16.17", "ip6": "2100:2100:2100:3::1", "updated": true }
|
||||||
|
]`,
|
||||||
|
expected: expected{progress: &SyncProgress{Complete: true, Updated: 2, Total: 2}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "record not found",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "test-zone",
|
||||||
|
apiResponse: `[]`,
|
||||||
|
expected: expected{errorMsg: "no nameservers records returned"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid json response",
|
||||||
|
authFQDN: "_acme-challenge.foo.com.",
|
||||||
|
zoneName: "test-zone",
|
||||||
|
apiResponse: `[x]`,
|
||||||
|
expected: expected{errorMsg: "failed to unmarshal UpdateRecord: invalid character 'x' looking for beginning of value: [x]"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
|
||||||
|
t.Cleanup(server.Close)
|
||||||
|
|
||||||
|
client, err := NewClient("myAuthID", "", "myAuthPassword")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.BaseURL, _ = url.Parse(server.URL)
|
||||||
|
|
||||||
|
syncProgress, err := client.GetUpdateStatus(test.zoneName)
|
||||||
|
|
||||||
|
if test.expected.errorMsg != "" {
|
||||||
|
require.EqualError(t, err, test.expected.errorMsg)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected.progress, syncProgress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
39
providers/dns/cloudns/internal/types.go
Normal file
39
providers/dns/cloudns/internal/types.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
type apiResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
StatusDescription string `json:"statusDescription"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone is a zone.
|
||||||
|
type Zone struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Zone string
|
||||||
|
Status string // is an integer, but cast as string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TXTRecord is a TXT record.
|
||||||
|
type TXTRecord struct {
|
||||||
|
ID int `json:"id,string"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Record string `json:"record"`
|
||||||
|
Failover int `json:"failover,string"`
|
||||||
|
TTL int `json:"ttl,string"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRecord is a Server Sync Record.
|
||||||
|
type UpdateRecord struct {
|
||||||
|
Server string `json:"server"`
|
||||||
|
IP4 string `json:"ip4"`
|
||||||
|
IP6 string `json:"ip6"`
|
||||||
|
Updated bool `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncProgress struct {
|
||||||
|
Complete bool
|
||||||
|
Updated int
|
||||||
|
Total int
|
||||||
|
}
|
Loading…
Reference in a new issue