1package linode
2
3import (
4	"encoding/json"
5	"fmt"
6	"net/http"
7	"net/http/httptest"
8	"os"
9	"testing"
10	"time"
11
12	"github.com/go-acme/lego/v3/platform/tester"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/require"
15	"github.com/timewasted/linode"
16	"github.com/timewasted/linode/dns"
17)
18
19type (
20	apiResponse struct {
21		Action string                 `json:"ACTION"`
22		Data   interface{}            `json:"DATA"`
23		Errors []linode.ResponseError `json:"ERRORARRAY"`
24	}
25	MockResponse struct {
26		Response interface{}
27		Errors   []linode.ResponseError
28	}
29	MockResponseMap map[string]MockResponse
30)
31
32var envTest = tester.NewEnvTest(EnvAPIKey)
33
34func newMockServer(responses MockResponseMap) *httptest.Server {
35	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36		// Ensure that we support the requested action.
37		action := r.URL.Query().Get("api_action")
38		resp, ok := responses[action]
39		if !ok {
40			http.Error(w, fmt.Sprintf("Unsupported mock action: %q", action), http.StatusInternalServerError)
41			return
42		}
43
44		// Build the response that the server will return.
45		response := apiResponse{
46			Action: action,
47			Data:   resp.Response,
48			Errors: resp.Errors,
49		}
50
51		rawResponse, err := json.Marshal(response)
52		if err != nil {
53			http.Error(w, fmt.Sprintf("Failed to JSON encode response: %v", err), http.StatusInternalServerError)
54			return
55		}
56
57		// Send the response.
58		w.Header().Set("Content-Type", "application/json")
59		w.WriteHeader(http.StatusOK)
60		_, err = w.Write(rawResponse)
61		if err != nil {
62			http.Error(w, err.Error(), http.StatusInternalServerError)
63			return
64		}
65	}))
66
67	time.Sleep(100 * time.Millisecond)
68	return srv
69}
70
71func TestNewDNSProvider(t *testing.T) {
72	testCases := []struct {
73		desc     string
74		envVars  map[string]string
75		expected string
76	}{
77		{
78			desc: "success",
79			envVars: map[string]string{
80				EnvAPIKey: "123",
81			},
82		},
83		{
84			desc: "missing api key",
85			envVars: map[string]string{
86				EnvAPIKey: "",
87			},
88			expected: "linode: some credentials information are missing: LINODE_API_KEY",
89		},
90	}
91
92	for _, test := range testCases {
93		t.Run(test.desc, func(t *testing.T) {
94			defer envTest.RestoreEnv()
95			envTest.ClearEnv()
96
97			envTest.Apply(test.envVars)
98
99			p, err := NewDNSProvider()
100
101			if len(test.expected) == 0 {
102				require.NoError(t, err)
103				require.NotNil(t, p)
104				require.NotNil(t, p.config)
105				require.NotNil(t, p.client)
106			} else {
107				require.EqualError(t, err, test.expected)
108			}
109		})
110	}
111}
112
113func TestNewDNSProviderConfig(t *testing.T) {
114	testCases := []struct {
115		desc     string
116		apiKey   string
117		expected string
118	}{
119		{
120			desc:   "success",
121			apiKey: "123",
122		},
123		{
124			desc:     "missing credentials",
125			expected: "linode: credentials missing",
126		},
127	}
128
129	for _, test := range testCases {
130		t.Run(test.desc, func(t *testing.T) {
131			config := NewDefaultConfig()
132			config.APIKey = test.apiKey
133
134			p, err := NewDNSProviderConfig(config)
135
136			if len(test.expected) == 0 {
137				require.NoError(t, err)
138				require.NotNil(t, p)
139				require.NotNil(t, p.config)
140				require.NotNil(t, p.client)
141			} else {
142				require.EqualError(t, err, test.expected)
143			}
144		})
145	}
146}
147
148func TestDNSProvider_Present(t *testing.T) {
149	defer envTest.RestoreEnv()
150	os.Setenv(EnvAPIKey, "testing")
151
152	p, err := NewDNSProvider()
153	require.NoError(t, err)
154
155	domain := "example.com"
156	keyAuth := "dGVzdGluZw=="
157
158	testCases := []struct {
159		desc          string
160		mockResponses MockResponseMap
161		expectedError string
162	}{
163		{
164			desc: "success",
165			mockResponses: MockResponseMap{
166				"domain.list": MockResponse{
167					Response: []dns.Domain{
168						{
169							Domain:   domain,
170							DomainID: 1234,
171						},
172					},
173				},
174				"domain.resource.create": MockResponse{
175					Response: dns.ResourceResponse{
176						ResourceID: 1234,
177					},
178				},
179			},
180		},
181		{
182			desc: "NoDomain",
183			mockResponses: MockResponseMap{
184				"domain.list": MockResponse{
185					Response: []dns.Domain{{
186						Domain:   "foobar.com",
187						DomainID: 1234,
188					}},
189				},
190			},
191			expectedError: "dns: requested domain not found",
192		},
193		{
194			desc: "CreateFailed",
195			mockResponses: MockResponseMap{
196				"domain.list": MockResponse{
197					Response: []dns.Domain{
198						{
199							Domain:   domain,
200							DomainID: 1234,
201						},
202					},
203				},
204				"domain.resource.create": MockResponse{
205					Response: nil,
206					Errors: []linode.ResponseError{
207						{
208							Code:    1234,
209							Message: "Failed to create domain resource",
210						},
211					},
212				},
213			},
214			expectedError: "Failed to create domain resource",
215		},
216	}
217
218	for _, test := range testCases {
219		t.Run(test.desc, func(t *testing.T) {
220			server := newMockServer(test.mockResponses)
221			defer server.Close()
222
223			p.client.ToLinode().SetEndpoint(server.URL)
224
225			err = p.Present(domain, "", keyAuth)
226			if len(test.expectedError) == 0 {
227				assert.NoError(t, err)
228			} else {
229				assert.EqualError(t, err, test.expectedError)
230			}
231		})
232	}
233}
234
235func TestDNSProvider_CleanUp(t *testing.T) {
236	defer envTest.RestoreEnv()
237	os.Setenv(EnvAPIKey, "testing")
238
239	p, err := NewDNSProvider()
240	require.NoError(t, err)
241
242	domain := "example.com"
243	keyAuth := "dGVzdGluZw=="
244
245	testCases := []struct {
246		desc          string
247		mockResponses MockResponseMap
248		expectedError string
249	}{
250		{
251			desc: "success",
252			mockResponses: MockResponseMap{
253				"domain.list": MockResponse{
254					Response: []dns.Domain{
255						{
256							Domain:   domain,
257							DomainID: 1234,
258						},
259					},
260				},
261				"domain.resource.list": MockResponse{
262					Response: []dns.Resource{
263						{
264							DomainID:   1234,
265							Name:       "_acme-challenge",
266							ResourceID: 1234,
267							Target:     "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
268							Type:       "TXT",
269						},
270					},
271				},
272				"domain.resource.delete": MockResponse{
273					Response: dns.ResourceResponse{
274						ResourceID: 1234,
275					},
276				},
277			},
278		},
279		{
280			desc: "NoDomain",
281			mockResponses: MockResponseMap{
282				"domain.list": MockResponse{
283					Response: []dns.Domain{
284						{
285							Domain:   "foobar.com",
286							DomainID: 1234,
287						},
288					},
289				},
290			},
291			expectedError: "dns: requested domain not found",
292		},
293		{
294			desc: "DeleteFailed",
295			mockResponses: MockResponseMap{
296				"domain.list": MockResponse{
297					Response: []dns.Domain{
298						{
299							Domain:   domain,
300							DomainID: 1234,
301						},
302					},
303				},
304				"domain.resource.list": MockResponse{
305					Response: []dns.Resource{
306						{
307							DomainID:   1234,
308							Name:       "_acme-challenge",
309							ResourceID: 1234,
310							Target:     "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
311							Type:       "TXT",
312						},
313					},
314				},
315				"domain.resource.delete": MockResponse{
316					Response: nil,
317					Errors: []linode.ResponseError{
318						{
319							Code:    1234,
320							Message: "Failed to delete domain resource",
321						},
322					},
323				},
324			},
325			expectedError: "Failed to delete domain resource",
326		},
327	}
328
329	for _, test := range testCases {
330		t.Run(test.desc, func(t *testing.T) {
331			server := newMockServer(test.mockResponses)
332			defer server.Close()
333
334			p.client.ToLinode().SetEndpoint(server.URL)
335
336			err = p.CleanUp(domain, "", keyAuth)
337			if len(test.expectedError) == 0 {
338				assert.NoError(t, err)
339			} else {
340				assert.EqualError(t, err, test.expectedError)
341			}
342		})
343	}
344}
345
346func TestLivePresent(t *testing.T) {
347	if !envTest.IsLiveTest() {
348		t.Skip("Skipping live test")
349	}
350	// TODO implement this test
351}
352
353func TestLiveCleanUp(t *testing.T) {
354	if !envTest.IsLiveTest() {
355		t.Skip("Skipping live test")
356	}
357	// TODO implement this test
358}
359