lego/providers/dns/acmedns/acmedns_test.go
Daniel McCarney 04e2d74406 DNS Providers: Add ACME-DNS provider. (#591)
This commit adds a new DNS provider for
[acme-dns](https://github.com/joohoi/acme-dns) to allow Lego to set
DNS-01 challenge response TXT with an ACME-DNS server automatically.
ACME-DNS allows ceding minimal zone editing permissions to the ACME
client and can be useful when the primary DNS provider for the zone does
not allow scripting/API access but can set a CNAME to an ACME-DNS
server.

Lower level ACME-DNS API calls & account loading/storing is handled by
the `github.com/cpu/goacmedns` library.

The provider loads existing ACME-DNS accounts from the specified JSON
file on disk. Any accounts the provider registers on behalf of the user
will also be saved to this JSON file.

When required, the provider handles registering accounts with the
ACME-DNS server domains that do not already have an ACME-DNS account.
This will halt issuance with an error prompting the user to set the
one-time manual CNAME required to delegate the DNS-01 challenge record
to the ACME-DNS server. Subsequent runs will use the account from disk
and assume the CNAME is in-place.
2018-07-09 19:28:01 +02:00

276 lines
7.7 KiB
Go

package acmedns
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cpu/goacmedns"
)
var (
// errorClientErr is used by the Client mocks that return an error.
errorClientErr = errors.New("errorClient always errors")
// errorStorageErr is used by the Storage mocks that return an error.
errorStorageErr = errors.New("errorStorage always errors")
// Fixed test data for unit tests.
egDomain = "threeletter.agency"
egFQDN = "_acme-challenge." + egDomain + "."
egKeyAuth = "⚷"
egAccount = goacmedns.Account{
FullDomain: "acme-dns." + egDomain,
SubDomain: "random-looking-junk." + egDomain,
Username: "spooky.mulder",
Password: "trustno1",
}
)
// mockClient is a mock implementing the acmeDNSClient interface that always
// returns a fixed goacmedns.Account from calls to Register.
type mockClient struct {
mockAccount goacmedns.Account
}
// UpdateTXTRecord does nothing.
func (c mockClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
return nil
}
// RegisterAccount returns c.mockAccount and no errors.
func (c mockClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
return c.mockAccount, nil
}
// mockUpdateClient is a mock implementing the acmeDNSClient interface that
// tracks the calls to UpdateTXTRecord in the records map.
type mockUpdateClient struct {
mockClient
records map[goacmedns.Account]string
}
// UpdateTXTRecord saves a record value to c.records for the given acct.
func (c mockUpdateClient) UpdateTXTRecord(acct goacmedns.Account, value string) error {
c.records[acct] = value
return nil
}
// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
// returns errors from errorUpdateClient.
type errorUpdateClient struct {
mockClient
}
// UpdateTXTRecord always returns an error.
func (c errorUpdateClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
return errorClientErr
}
// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
// returns errors from RegisterAccount.
type errorRegisterClient struct {
mockClient
}
// RegisterAccount always returns an error.
func (c errorRegisterClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
return goacmedns.Account{}, errorClientErr
}
// mockStorage is a mock implementing the goacmedns.Storage interface that
// returns static account data and ignores Save.
type mockStorage struct {
accounts map[string]goacmedns.Account
}
// Save does nothing.
func (m mockStorage) Save() error {
return nil
}
// Put stores an account for the given domain in m.accounts.
func (m mockStorage) Put(domain string, acct goacmedns.Account) error {
m.accounts[domain] = acct
return nil
}
// Fetch retrieves an account for the given domain from m.accounts or returns
// goacmedns.ErrDomainNotFound.
func (m mockStorage) Fetch(domain string) (goacmedns.Account, error) {
if acct, ok := m.accounts[domain]; ok {
return acct, nil
}
return goacmedns.Account{}, goacmedns.ErrDomainNotFound
}
// errorPutStorage is a mock implementing the goacmedns.Storage interface that
// always returns errors from Put.
type errorPutStorage struct {
mockStorage
}
// Put always errors.
func (e errorPutStorage) Put(_ string, _ goacmedns.Account) error {
return errorStorageErr
}
// errorSaveStoragr is a mock implementing the goacmedns.Storage interface that
// always returns errors from Save.
type errorSaveStorage struct {
mockStorage
}
// Save always errors.
func (e errorSaveStorage) Save() error {
return errorStorageErr
}
// errorFetchStorage is a mock implementing the goacmedns.Storage interface that
// always returns errors from Fetch.
type errorFetchStorage struct {
mockStorage
}
// Fetch always errors.
func (e errorFetchStorage) Fetch(_ string) (goacmedns.Account, error) {
return goacmedns.Account{}, errorStorageErr
}
// TestPresent tests that the ACME-DNS Present function for updating a DNS-01
// challenge response TXT record works as expected.
func TestPresent(t *testing.T) {
// validAccountStorage is a mockStorage configured to return the egAccount.
validAccountStorage := mockStorage{
map[string]goacmedns.Account{
egDomain: egAccount,
},
}
// validUpdateClient is a mockClient configured with the egAccount that will
// track TXT updates in a map.
validUpdateClient := mockUpdateClient{
mockClient{egAccount},
make(map[goacmedns.Account]string),
}
testCases := []struct {
Name string
Client acmeDNSClient
Storage goacmedns.Storage
ExpectedError error
}{
{
Name: "present when client storage returns unexpected error",
Client: mockClient{egAccount},
Storage: errorFetchStorage{},
ExpectedError: errorStorageErr,
},
{
Name: "present when client storage returns ErrDomainNotFound",
Client: mockClient{egAccount},
ExpectedError: ErrCNAMERequired{
Domain: egDomain,
FQDN: egFQDN,
Target: egAccount.FullDomain,
},
},
{
Name: "present when client UpdateTXTRecord returns unexpected error",
Client: errorUpdateClient{},
Storage: validAccountStorage,
ExpectedError: errorClientErr,
},
{
Name: "present when everything works",
Storage: validAccountStorage,
Client: validUpdateClient,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
dp, err := NewDNSProviderClient(tc.Client, mockStorage{make(map[string]goacmedns.Account)})
require.NoError(t, err)
// override the storage mock if required by the testcase.
if tc.Storage != nil {
dp.storage = tc.Storage
}
// call Present. The token argument can be garbage because the ACME-DNS
// provider does not use it.
err = dp.Present(egDomain, "foo", egKeyAuth)
if tc.ExpectedError != nil {
assert.Equal(t, tc.ExpectedError, err)
} else {
require.NoError(t, err)
}
})
}
// Check that the success testcase set a record.
assert.Len(t, validUpdateClient.records, 1)
// Check that the success testcase set the right record for the right account.
assert.Len(t, validUpdateClient.records[egAccount], 43)
}
// TestRegister tests that the ACME-DNS register function works correctly.
func TestRegister(t *testing.T) {
testCases := []struct {
Name string
Client acmeDNSClient
Storage goacmedns.Storage
Domain string
FQDN string
ExpectedError error
}{
{
Name: "register when acme-dns client returns an error",
Client: errorRegisterClient{},
ExpectedError: errorClientErr,
},
{
Name: "register when acme-dns storage put returns an error",
Client: mockClient{egAccount},
Storage: errorPutStorage{mockStorage{make(map[string]goacmedns.Account)}},
ExpectedError: errorStorageErr,
},
{
Name: "register when acme-dns storage save returns an error",
Client: mockClient{egAccount},
Storage: errorSaveStorage{mockStorage{make(map[string]goacmedns.Account)}},
ExpectedError: errorStorageErr,
},
{
Name: "register when everything works",
Client: mockClient{egAccount},
ExpectedError: ErrCNAMERequired{
Domain: egDomain,
FQDN: egFQDN,
Target: egAccount.FullDomain,
},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
dp, err := NewDNSProviderClient(tc.Client, mockStorage{make(map[string]goacmedns.Account)})
require.NoError(t, err)
// override the storage mock if required by the testcase.
if tc.Storage != nil {
dp.storage = tc.Storage
}
// Call register for the example domain/fqdn.
err = dp.register(egDomain, egFQDN)
if tc.ExpectedError != nil {
assert.Equal(t, tc.ExpectedError, err)
} else {
require.NoError(t, err)
}
})
}
}