forked from TrueCloudLab/lego
godaddy: add tests on the client (#1521)
This commit is contained in:
parent
60ae6e6dc9
commit
15f3a45342
6 changed files with 251 additions and 29 deletions
|
@ -10,13 +10,10 @@ import (
|
|||
|
||||
"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/godaddy/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultBaseURL represents the API endpoint to call.
|
||||
defaultBaseURL = "https://api.godaddy.com"
|
||||
minTTL = 600
|
||||
)
|
||||
const minTTL = 600
|
||||
|
||||
// Environment variables names.
|
||||
const (
|
||||
|
@ -56,6 +53,7 @@ func NewDefaultConfig() *Config {
|
|||
// DNSProvider implements the challenge.Provider interface.
|
||||
type DNSProvider struct {
|
||||
config *Config
|
||||
client *internal.Client
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance configured for godaddy.
|
||||
|
@ -88,7 +86,13 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
|||
return nil, fmt.Errorf("godaddy: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
|
||||
}
|
||||
|
||||
return &DNSProvider{config: config}, nil
|
||||
client := internal.NewClient(config.APIKey, config.APISecret)
|
||||
|
||||
if config.HTTPClient != nil {
|
||||
client.HTTPClient = config.HTTPClient
|
||||
}
|
||||
|
||||
return &DNSProvider{config: config, client: client}, nil
|
||||
}
|
||||
|
||||
// Timeout returns the timeout and interval to use when checking for DNS
|
||||
|
@ -108,19 +112,19 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
|||
|
||||
recordName := extractRecordName(fqdn, domainZone)
|
||||
|
||||
records, err := d.getRecords(domainZone, "TXT", recordName)
|
||||
records, err := d.client.GetRecords(domainZone, "TXT", recordName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("godaddy: failed to get TXT records: %w", err)
|
||||
}
|
||||
|
||||
var newRecords []DNSRecord
|
||||
var newRecords []internal.DNSRecord
|
||||
for _, record := range records {
|
||||
if record.Data != "" {
|
||||
newRecords = append(newRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
record := DNSRecord{
|
||||
record := internal.DNSRecord{
|
||||
Type: "TXT",
|
||||
Name: recordName,
|
||||
Data: value,
|
||||
|
@ -128,7 +132,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
|||
}
|
||||
newRecords = append(newRecords, record)
|
||||
|
||||
err = d.updateTxtRecords(newRecords, domainZone, recordName)
|
||||
err = d.client.UpdateTxtRecords(newRecords, domainZone, recordName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("godaddy: failed to add TXT record: %w", err)
|
||||
}
|
||||
|
@ -147,7 +151,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
|
||||
recordName := extractRecordName(fqdn, domainZone)
|
||||
|
||||
records, err := d.getRecords(domainZone, "TXT", recordName)
|
||||
records, err := d.client.GetRecords(domainZone, "TXT", recordName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("godaddy: failed to get TXT records: %w", err)
|
||||
}
|
||||
|
@ -156,12 +160,12 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
allTxtRecords, err := d.getRecords(domainZone, "TXT", "")
|
||||
allTxtRecords, err := d.client.GetRecords(domainZone, "TXT", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("godaddy: failed to get all TXT records: %w", err)
|
||||
}
|
||||
|
||||
var recordsKeep []DNSRecord
|
||||
var recordsKeep []internal.DNSRecord
|
||||
for _, record := range allTxtRecords {
|
||||
if record.Data != value && record.Data != "" {
|
||||
recordsKeep = append(recordsKeep, record)
|
||||
|
@ -170,11 +174,11 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
|||
|
||||
// GoDaddy API don't provide a way to delete a record, an "empty" record must be added.
|
||||
if len(recordsKeep) == 0 {
|
||||
emptyRecord := DNSRecord{Name: "empty", Data: ""}
|
||||
emptyRecord := internal.DNSRecord{Name: "empty", Data: ""}
|
||||
recordsKeep = append(recordsKeep, emptyRecord)
|
||||
}
|
||||
|
||||
err = d.updateTxtRecords(recordsKeep, domainZone, "")
|
||||
err = d.client.UpdateTxtRecords(recordsKeep, domainZone, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("godaddy: failed to remove TXT record: %w", err)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package godaddy
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -6,19 +6,33 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DNSRecord a DNS record.
|
||||
type DNSRecord struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Data string `json:"data"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
// DefaultBaseURL represents the API endpoint to call.
|
||||
const DefaultBaseURL = "https://api.godaddy.com"
|
||||
|
||||
type Client struct {
|
||||
HTTPClient *http.Client
|
||||
baseURL *url.URL
|
||||
apiKey string
|
||||
apiSecret string
|
||||
}
|
||||
|
||||
func (d *DNSProvider) getRecords(domainZone, rType, recordName string) ([]DNSRecord, error) {
|
||||
func NewClient(apiKey string, apiSecret string) *Client {
|
||||
baseURL, _ := url.Parse(DefaultBaseURL)
|
||||
|
||||
return &Client{
|
||||
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||
baseURL: baseURL,
|
||||
apiKey: apiKey,
|
||||
apiSecret: apiSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Client) GetRecords(domainZone, rType, recordName string) ([]DNSRecord, error) {
|
||||
resource := path.Clean(fmt.Sprintf("/v1/domains/%s/records/%s/%s", domainZone, rType, recordName))
|
||||
|
||||
resp, err := d.makeRequest(http.MethodGet, resource, nil)
|
||||
|
@ -43,7 +57,7 @@ func (d *DNSProvider) getRecords(domainZone, rType, recordName string) ([]DNSRec
|
|||
return records, nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) updateTxtRecords(records []DNSRecord, domainZone, recordName string) error {
|
||||
func (d *Client) UpdateTxtRecords(records []DNSRecord, domainZone, recordName string) error {
|
||||
body, err := json.Marshal(records)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -67,15 +81,20 @@ func (d *DNSProvider) updateTxtRecords(records []DNSRecord, domainZone, recordNa
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSProvider) makeRequest(method, uri string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", defaultBaseURL, uri), body)
|
||||
func (d *Client) makeRequest(method, uri string, body io.Reader) (*http.Response, error) {
|
||||
endpoint, err := d.baseURL.Parse(path.Join(d.baseURL.Path, uri))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, endpoint.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.config.APIKey, d.config.APISecret))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", d.apiKey, d.apiSecret))
|
||||
|
||||
return d.config.HTTPClient.Do(req)
|
||||
return d.HTTPClient.Do(req)
|
||||
}
|
142
providers/dns/godaddy/internal/client_test.go
Normal file
142
providers/dns/godaddy/internal/client_test.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTest(t *testing.T) (*http.ServeMux, *Client) {
|
||||
t.Helper()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := NewClient("key", "secret")
|
||||
client.HTTPClient = server.Client()
|
||||
client.baseURL, _ = url.Parse(server.URL)
|
||||
|
||||
return mux, client
|
||||
}
|
||||
|
||||
func TestClient_GetRecords(t *testing.T) {
|
||||
mux, client := setupTest(t)
|
||||
|
||||
mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusOK, "getrecords.json"))
|
||||
|
||||
records, err := client.GetRecords("example.com", "TXT", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := []DNSRecord{
|
||||
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
|
||||
{Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600},
|
||||
{Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, records)
|
||||
}
|
||||
|
||||
func TestClient_GetRecords_errors(t *testing.T) {
|
||||
mux, client := setupTest(t)
|
||||
|
||||
mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusUnprocessableEntity, "errors.json"))
|
||||
|
||||
records, err := client.GetRecords("example.com", "TXT", "")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, records)
|
||||
}
|
||||
|
||||
func TestClient_UpdateTxtRecords(t *testing.T) {
|
||||
mux, client := setupTest(t)
|
||||
|
||||
mux.HandleFunc("/v1/domains/example.com/records/TXT/lego", func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPut {
|
||||
http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "sso-key key:secret" {
|
||||
http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
records := []DNSRecord{
|
||||
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
|
||||
{Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600},
|
||||
{Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600},
|
||||
}
|
||||
|
||||
err := client.UpdateTxtRecords(records, "example.com", "lego")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_UpdateTxtRecords_errors(t *testing.T) {
|
||||
mux, client := setupTest(t)
|
||||
|
||||
mux.HandleFunc("/v1/domains/example.com/records/TXT/lego",
|
||||
testHandler(http.MethodPut, http.StatusUnprocessableEntity, "errors.json"))
|
||||
|
||||
records := []DNSRecord{
|
||||
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
|
||||
{Name: "_acme-challenge.example", Type: "TXT", Data: "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU", TTL: 600},
|
||||
{Name: "_acme-challenge.example", Type: "TXT", Data: "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: " ", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A", TTL: 600},
|
||||
{Name: "_acme-challenge.lego", Type: "TXT", Data: "acme", TTL: 600},
|
||||
}
|
||||
|
||||
err := client.UpdateTxtRecords(records, "example.com", "lego")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != method {
|
||||
http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "sso-key key:secret" {
|
||||
http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
rw.WriteHeader(statusCode)
|
||||
|
||||
if statusCode == http.StatusNoContent {
|
||||
return
|
||||
}
|
||||
|
||||
file, err := os.Open(filepath.Join("fixtures", filename))
|
||||
if err != nil {
|
||||
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
_, err = io.Copy(rw, file)
|
||||
if err != nil {
|
||||
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
4
providers/dns/godaddy/internal/fixtures/errors.json
Normal file
4
providers/dns/godaddy/internal/fixtures/errors.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"code": "INVALID_BODY",
|
||||
"message": "Request body doesn't fulfill schema, see details in `fields`"
|
||||
}
|
38
providers/dns/godaddy/internal/fixtures/getrecords.json
Normal file
38
providers/dns/godaddy/internal/fixtures/getrecords.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
[
|
||||
{
|
||||
"name":"_acme-challenge",
|
||||
"type":"TXT",
|
||||
"data":" ",
|
||||
"ttl":600
|
||||
},
|
||||
{
|
||||
"name":"_acme-challenge.example",
|
||||
"type":"TXT",
|
||||
"data":"6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU",
|
||||
"ttl":600
|
||||
},
|
||||
{
|
||||
"name":"_acme-challenge.example",
|
||||
"type":"TXT",
|
||||
"data":"8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek",
|
||||
"ttl":600
|
||||
},
|
||||
{
|
||||
"name":"_acme-challenge.lego",
|
||||
"type":"TXT",
|
||||
"data":" ",
|
||||
"ttl":600
|
||||
},
|
||||
{
|
||||
"name":"_acme-challenge.lego",
|
||||
"type":"TXT",
|
||||
"data":"0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A",
|
||||
"ttl":600
|
||||
},
|
||||
{
|
||||
"name":"_acme-challenge.lego",
|
||||
"type":"TXT",
|
||||
"data":"acme",
|
||||
"ttl":600
|
||||
}
|
||||
]
|
15
providers/dns/godaddy/internal/types.go
Normal file
15
providers/dns/godaddy/internal/types.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package internal
|
||||
|
||||
// DNSRecord a DNS record.
|
||||
type DNSRecord struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Data string `json:"data"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
}
|
Loading…
Reference in a new issue