forked from TrueCloudLab/lego
612 lines
16 KiB
Go
612 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)
|
|
})
|
|
}
|
|
}
|