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