1package acmedns
2
3import (
4	"errors"
5	"testing"
6
7	"github.com/cpu/goacmedns"
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10)
11
12var (
13	// errorClientErr is used by the Client mocks that return an error.
14	errorClientErr = errors.New("errorClient always errors")
15	// errorStorageErr is used by the Storage mocks that return an error.
16	errorStorageErr = errors.New("errorStorage always errors")
17)
18
19const (
20	// Fixed test data for unit tests.
21	egDomain  = "threeletter.agency"
22	egFQDN    = "_acme-challenge." + egDomain + "."
23	egKeyAuth = "⚷"
24)
25
26var egTestAccount = goacmedns.Account{
27	FullDomain: "acme-dns." + egDomain,
28	SubDomain:  "random-looking-junk." + egDomain,
29	Username:   "spooky.mulder",
30	Password:   "trustno1",
31}
32
33// mockClient is a mock implementing the acmeDNSClient interface that always
34// returns a fixed goacmedns.Account from calls to Register.
35type mockClient struct {
36	mockAccount goacmedns.Account
37}
38
39// UpdateTXTRecord does nothing.
40func (c mockClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
41	return nil
42}
43
44// RegisterAccount returns c.mockAccount and no errors.
45func (c mockClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
46	return c.mockAccount, nil
47}
48
49// mockUpdateClient is a mock implementing the acmeDNSClient interface that
50// tracks the calls to UpdateTXTRecord in the records map.
51type mockUpdateClient struct {
52	mockClient
53	records map[goacmedns.Account]string
54}
55
56// UpdateTXTRecord saves a record value to c.records for the given acct.
57func (c mockUpdateClient) UpdateTXTRecord(acct goacmedns.Account, value string) error {
58	c.records[acct] = value
59	return nil
60}
61
62// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
63// returns errors from errorUpdateClient.
64type errorUpdateClient struct {
65	mockClient
66}
67
68// UpdateTXTRecord always returns an error.
69func (c errorUpdateClient) UpdateTXTRecord(_ goacmedns.Account, _ string) error {
70	return errorClientErr
71}
72
73// errorRegisterClient is a mock implementing the acmeDNSClient interface that always
74// returns errors from RegisterAccount.
75type errorRegisterClient struct {
76	mockClient
77}
78
79// RegisterAccount always returns an error.
80func (c errorRegisterClient) RegisterAccount(_ []string) (goacmedns.Account, error) {
81	return goacmedns.Account{}, errorClientErr
82}
83
84// mockStorage is a mock implementing the goacmedns.Storage interface that
85// returns static account data and ignores Save.
86type mockStorage struct {
87	accounts map[string]goacmedns.Account
88}
89
90// Save does nothing.
91func (m mockStorage) Save() error {
92	return nil
93}
94
95// Put stores an account for the given domain in m.accounts.
96func (m mockStorage) Put(domain string, acct goacmedns.Account) error {
97	m.accounts[domain] = acct
98	return nil
99}
100
101// Fetch retrieves an account for the given domain from m.accounts or returns
102// goacmedns.ErrDomainNotFound.
103func (m mockStorage) Fetch(domain string) (goacmedns.Account, error) {
104	if acct, ok := m.accounts[domain]; ok {
105		return acct, nil
106	}
107	return goacmedns.Account{}, goacmedns.ErrDomainNotFound
108}
109
110// FetchAll returns all of m.accounts.
111func (m mockStorage) FetchAll() map[string]goacmedns.Account {
112	return m.accounts
113}
114
115// errorPutStorage is a mock implementing the goacmedns.Storage interface that
116// always returns errors from Put.
117type errorPutStorage struct {
118	mockStorage
119}
120
121// Put always errors.
122func (e errorPutStorage) Put(_ string, _ goacmedns.Account) error {
123	return errorStorageErr
124}
125
126// errorSaveStorage is a mock implementing the goacmedns.Storage interface that
127// always returns errors from Save.
128type errorSaveStorage struct {
129	mockStorage
130}
131
132// Save always errors.
133func (e errorSaveStorage) Save() error {
134	return errorStorageErr
135}
136
137// errorFetchStorage is a mock implementing the goacmedns.Storage interface that
138// always returns errors from Fetch.
139type errorFetchStorage struct {
140	mockStorage
141}
142
143// Fetch always errors.
144func (e errorFetchStorage) Fetch(_ string) (goacmedns.Account, error) {
145	return goacmedns.Account{}, errorStorageErr
146}
147
148// FetchAll is a nop for errorFetchStorage.
149func (e errorFetchStorage) FetchAll() map[string]goacmedns.Account {
150	return nil
151}
152
153// TestPresent tests that the ACME-DNS Present function for updating a DNS-01
154// challenge response TXT record works as expected.
155func TestPresent(t *testing.T) {
156	// validAccountStorage is a mockStorage configured to return the egTestAccount.
157	validAccountStorage := mockStorage{
158		map[string]goacmedns.Account{
159			egDomain: egTestAccount,
160		},
161	}
162	// validUpdateClient is a mockClient configured with the egTestAccount that will
163	// track TXT updates in a map.
164	validUpdateClient := mockUpdateClient{
165		mockClient{egTestAccount},
166		make(map[goacmedns.Account]string),
167	}
168
169	testCases := []struct {
170		Name          string
171		Client        acmeDNSClient
172		Storage       goacmedns.Storage
173		ExpectedError error
174	}{
175		{
176			Name:          "present when client storage returns unexpected error",
177			Client:        mockClient{egTestAccount},
178			Storage:       errorFetchStorage{},
179			ExpectedError: errorStorageErr,
180		},
181		{
182			Name:   "present when client storage returns ErrDomainNotFound",
183			Client: mockClient{egTestAccount},
184			ExpectedError: ErrCNAMERequired{
185				Domain: egDomain,
186				FQDN:   egFQDN,
187				Target: egTestAccount.FullDomain,
188			},
189		},
190		{
191			Name:          "present when client UpdateTXTRecord returns unexpected error",
192			Client:        errorUpdateClient{},
193			Storage:       validAccountStorage,
194			ExpectedError: errorClientErr,
195		},
196		{
197			Name:    "present when everything works",
198			Storage: validAccountStorage,
199			Client:  validUpdateClient,
200		},
201	}
202
203	for _, test := range testCases {
204		t.Run(test.Name, func(t *testing.T) {
205			dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)})
206			require.NoError(t, err)
207
208			// override the storage mock if required by the test case.
209			if test.Storage != nil {
210				dp.storage = test.Storage
211			}
212
213			// call Present. The token argument can be garbage because the ACME-DNS
214			// provider does not use it.
215			err = dp.Present(egDomain, "foo", egKeyAuth)
216			if test.ExpectedError != nil {
217				assert.Equal(t, test.ExpectedError, err)
218			} else {
219				require.NoError(t, err)
220			}
221		})
222	}
223
224	// Check that the success test case set a record.
225	assert.Len(t, validUpdateClient.records, 1)
226
227	// Check that the success test case set the right record for the right account.
228	assert.Len(t, validUpdateClient.records[egTestAccount], 43)
229}
230
231// TestRegister tests that the ACME-DNS register function works correctly.
232func TestRegister(t *testing.T) {
233	testCases := []struct {
234		Name          string
235		Client        acmeDNSClient
236		Storage       goacmedns.Storage
237		Domain        string
238		FQDN          string
239		ExpectedError error
240	}{
241		{
242			Name:          "register when acme-dns client returns an error",
243			Client:        errorRegisterClient{},
244			ExpectedError: errorClientErr,
245		},
246		{
247			Name:          "register when acme-dns storage put returns an error",
248			Client:        mockClient{egTestAccount},
249			Storage:       errorPutStorage{mockStorage{make(map[string]goacmedns.Account)}},
250			ExpectedError: errorStorageErr,
251		},
252		{
253			Name:          "register when acme-dns storage save returns an error",
254			Client:        mockClient{egTestAccount},
255			Storage:       errorSaveStorage{mockStorage{make(map[string]goacmedns.Account)}},
256			ExpectedError: errorStorageErr,
257		},
258		{
259			Name:   "register when everything works",
260			Client: mockClient{egTestAccount},
261			ExpectedError: ErrCNAMERequired{
262				Domain: egDomain,
263				FQDN:   egFQDN,
264				Target: egTestAccount.FullDomain,
265			},
266		},
267	}
268
269	for _, test := range testCases {
270		t.Run(test.Name, func(t *testing.T) {
271			dp, err := NewDNSProviderClient(test.Client, mockStorage{make(map[string]goacmedns.Account)})
272			require.NoError(t, err)
273
274			// override the storage mock if required by the testcase.
275			if test.Storage != nil {
276				dp.storage = test.Storage
277			}
278
279			// Call register for the example domain/fqdn.
280			err = dp.register(egDomain, egFQDN)
281			if test.ExpectedError != nil {
282				assert.Equal(t, test.ExpectedError, err)
283			} else {
284				require.NoError(t, err)
285			}
286		})
287	}
288}
289