lego/providers/dns/cloudns/internal/client_test.go
2024-02-24 20:49:40 +01:00

613 lines
16 KiB
Go

package internal
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTest(t *testing.T, subAuthID string, handler http.HandlerFunc) *Client {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
client, err := NewClient("myAuthID", subAuthID, "myAuthPassword")
require.NoError(t, err)
client.BaseURL, _ = url.Parse(server.URL)
client.HTTPClient = server.Client()
return client
}
func handlerMock(method string, jsonData []byte) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
if req.Method != method {
http.Error(rw, "Incorrect method used", http.StatusBadRequest)
return
}
_, err := rw.Write(jsonData)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
}
}
func TestNewClient(t *testing.T) {
testCases := []struct {
desc string
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 {
desc string
authFQDN string
apiResponse string
expected
}{
{
desc: "zone found",
authFQDN: "_acme-challenge.foo.com.",
apiResponse: `{"name": "foo.com", "type": "master", "zone": "zone", "status": "1"}`,
expected: expected{
zone: &Zone{
Name: "foo.com",
Type: "master",
Zone: "zone",
Status: "1",
},
},
},
{
desc: "zone not found",
authFQDN: "_acme-challenge.foo.com.",
apiResponse: ``,
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: "unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.Zone",
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse)))
zone, err := client.GetZone(context.Background(), test.authFQDN)
if test.expected.errorMsg != "" {
require.EqualError(t, err, test.expected.errorMsg)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected.zone, zone)
}
})
}
}
func TestClient_FindTxtRecord(t *testing.T) {
type expected struct {
txtRecord *TXTRecord
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{
txtRecord: &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.example.com.",
zoneName: "example.com",
apiResponse: `[]`,
},
{
desc: "invalid json response",
authFQDN: "_acme-challenge.example.com.",
zoneName: "example.com",
apiResponse: `[{}]`,
expected: expected{
errorMsg: "unable to unmarshal response: [status code: 200] body: [{}] error: 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) {
client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse)))
txtRecord, err := client.FindTxtRecord(context.Background(), 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.txtRecord, txtRecord)
}
})
}
}
func TestClient_ListTxtRecord(t *testing.T) {
type expected struct {
txtRecords []TXTRecord
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.example.com.",
zoneName: "example.com",
apiResponse: `[]`,
},
{
desc: "invalid json response",
authFQDN: "_acme-challenge.example.com.",
zoneName: "example.com",
apiResponse: `[{}]`,
expected: expected{
errorMsg: "unable to unmarshal response: [status code: 200] body: [{}] error: 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) {
client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse)))
txtRecords, err := client.ListTxtRecords(context.Background(), 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 {
desc string
authID string
subAuthID string
zoneName string
authFQDN string
value string
ttl int
apiResponse string
expected
}{
{
desc: "sub-zone",
authID: "myAuthID",
zoneName: "example.com",
authFQDN: "_acme-challenge.foo.example.com.",
value: "txtTXTtxtTXTtxtTXTtxtTXT",
ttl: 60,
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
expected: expected{
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`,
},
},
{
desc: "main zone (authID)",
authID: "myAuthID",
zoneName: "example.com",
authFQDN: "_acme-challenge.example.com.",
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
ttl: 60,
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
expected: expected{
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`,
},
},
{
desc: "main zone (subAuthID)",
subAuthID: "mySubAuthID",
zoneName: "example.com",
authFQDN: "_acme-challenge.example.com.",
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
ttl: 60,
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
expected: expected{
query: `auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`,
},
},
{
desc: "invalid status",
authID: "myAuthID",
zoneName: "example.com",
authFQDN: "_acme-challenge.example.com.",
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
ttl: 120,
apiResponse: `{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`,
expected: expected{
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
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: "example.com",
authFQDN: "_acme-challenge.example.com.",
value: "TXTtxtTXTtxtTXTtxtTXTtxt",
ttl: 120,
apiResponse: `[{}]`,
expected: expected{
query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
errorMsg: "unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.apiResponse",
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
client := setupTest(t, test.subAuthID, 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)
})
err := client.AddTxtRecord(context.Background(), test.zoneName, test.authFQDN, test.value, test.ttl)
if test.expected.errorMsg != "" {
require.EqualError(t, err, test.expected.errorMsg)
} else {
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: "unable to unmarshal response: [status code: 200] body: [{}] error: 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(context.Background(), 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: "unable to unmarshal response: [status code: 200] body: [x] error: invalid character 'x' looking for beginning of value"},
},
}
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(context.Background(), 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)
})
}
}