package dmapi import ( "io" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( correctAPIKey = "123" incorrectAPIKey = "321" serverErrorAPIKey = "500" ) const ( correctUsername = "lego" incorrectUsername = "not_lego" serverErrorUsername = "error" ) func setup(t *testing.T) (*http.ServeMux, string) { t.Helper() mux := http.NewServeMux() server := httptest.NewServer(mux) t.Cleanup(server.Close) return mux, server.URL } func TestDNSProvider_login_api_key(t *testing.T) { testCases := []struct { desc string apiKey string expectedError bool expectedStatusCode int expectedAuthSid string }{ { desc: "correct key", apiKey: correctAPIKey, expectedStatusCode: 0, expectedAuthSid: correctAPIKey, }, { desc: "incorrect key", apiKey: incorrectAPIKey, expectedStatusCode: 2200, expectedError: true, }, { desc: "server error", apiKey: serverErrorAPIKey, expectedStatusCode: -500, expectedError: true, }, { desc: "non-ok status code", apiKey: "333", expectedStatusCode: 2202, expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) switch r.FormValue("api-key") { case correctAPIKey: _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") case incorrectAPIKey: _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") case serverErrorAPIKey: http.NotFound(w, r) default: _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{APIKey: test.apiKey}) client.BaseURL = serverURL response, err := client.Login() if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) assert.Equal(t, test.expectedAuthSid, response.AuthSid) } }) } } func TestDNSProvider_login_username(t *testing.T) { testCases := []struct { desc string username string password string expectedError bool expectedStatusCode int expectedAuthSid string }{ { desc: "correct username and password", username: correctUsername, password: "go-acme", expectedError: false, expectedStatusCode: 0, expectedAuthSid: correctAPIKey, }, { desc: "incorrect username", username: incorrectUsername, password: "go-acme", expectedStatusCode: 2200, expectedError: true, }, { desc: "server error", username: serverErrorUsername, password: "go-acme", expectedStatusCode: -500, expectedError: true, }, { desc: "non-ok status code", username: "random", password: "go-acme", expectedStatusCode: 2202, expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) switch r.FormValue("username") { case correctUsername: _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet") case incorrectUsername: _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") case serverErrorUsername: http.NotFound(w, r) default: _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet") } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{Username: test.username, Password: test.password}) client.BaseURL = serverURL response, err := client.Login() if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) assert.Equal(t, test.expectedAuthSid, response.AuthSid) } }) } } func TestDNSProvider_logout(t *testing.T) { testCases := []struct { desc string authSid string expectedError bool expectedStatusCode int }{ { desc: "correct auth-sid", authSid: correctAPIKey, expectedStatusCode: 0, }, { desc: "incorrect auth-sid", authSid: incorrectAPIKey, expectedStatusCode: 2200, }, { desc: "already logged out", authSid: "", expectedError: true, }, { desc: "server error", authSid: serverErrorAPIKey, expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) switch r.FormValue("auth-sid") { case correctAPIKey: _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n") case incorrectAPIKey: _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error") default: http.NotFound(w, r) } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid}) client.BaseURL = serverURL response, err := client.Logout() if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) } }) } } func TestDNSProvider_getZone(t *testing.T) { testZone := "@ A 0 192.0.2.2 3600" testCases := []struct { desc string authSid string domain string zone string expectedError bool expectedStatusCode int }{ { desc: "correct auth-sid, known domain", authSid: correctAPIKey, domain: "known", zone: testZone, expectedStatusCode: 0, }, { desc: "incorrect auth-sid, known domain", authSid: incorrectAPIKey, domain: "known", expectedStatusCode: 2202, }, { desc: "correct auth-sid, unknown domain", authSid: correctAPIKey, domain: "unknown", expectedStatusCode: 2202, }, { desc: "server error", authSid: "500", expectedError: true, }, } mux, serverURL := setup(t) mux.HandleFunc("/dns-zone-get", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) authSid := r.FormValue("auth-sid") domain := r.FormValue("domain") switch { case authSid == correctAPIKey && domain == "known": _, _ = io.WriteString(w, "Status-Code: 0\nStatus-Text: OK\n\n"+testZone) case authSid == incorrectAPIKey || (authSid == correctAPIKey && domain == "unknown"): _, _ = io.WriteString(w, "Status-Code: 2202\nStatus-Text: Authorization error") default: http.NotFound(w, r) } }) for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { client := NewClient(AuthInfo{APIKey: "12345", authSid: test.authSid}) client.BaseURL = serverURL response, err := client.GetZone(test.domain) if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, response) assert.Equal(t, test.expectedStatusCode, response.StatusCode) assert.Equal(t, test.zone, response.Body) } }) } } func Test_parseResponse(t *testing.T) { testCases := []struct { desc string input string expected *Response }{ { desc: "Empty response", input: "", expected: &Response{ Headers: url.Values{}, StatusCode: -1, }, }, { desc: "No headers, just body", input: "\n\nTest body", expected: &Response{ Headers: url.Values{}, Body: "Test body", StatusCode: -1, }, }, { desc: "Headers and body", input: "Test-Header: value\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}}, Body: "Test body", StatusCode: -1, }, }, { desc: "Headers and body + Auth-Sid", input: "Test-Header: value\nAuth-Sid: 123\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}, "Auth-Sid": {"123"}}, Body: "Test body", StatusCode: -1, AuthSid: "123", }, }, { desc: "Headers and body + Status-Text", input: "Test-Header: value\nStatus-Text: OK\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}, "Status-Text": {"OK"}}, Body: "Test body", StatusText: "OK", StatusCode: -1, }, }, { desc: "Headers and body + Status-Code", input: "Test-Header: value\nStatus-Code: 2020\n\nTest body", expected: &Response{ Headers: url.Values{"Test-Header": {"value"}, "Status-Code": {"2020"}}, Body: "Test body", StatusCode: 2020, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() response := parseResponse(test.input) assert.Equal(t, test.expected, response) }) } } func Test_removeTxtEntryFromZone(t *testing.T) { testCases := []struct { desc string input string expected string modified bool }{ { desc: "empty zone", input: "", expected: "", modified: false, }, { desc: "zone with only A entry", input: "@ A 0 192.0.2.2 3600", expected: "@ A 0 192.0.2.2 3600", modified: false, }, { desc: "zone with only clenup entry", input: "_acme-challenge TXT 0 \"old \" 120", expected: "", modified: true, }, { desc: "zone with one A and one cleanup entries", input: "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0 \"old \" 120", expected: "@ A 0 192.0.2.2 3600", modified: true, }, { desc: "zone with one A and multiple cleanup entries", input: "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0 \"old \" 120\n_acme-challenge TXT 0 \"another \" 120", expected: "@ A 0 192.0.2.2 3600", modified: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() zone, modified := RemoveTxtEntryFromZone(test.input, "_acme-challenge") assert.Equal(t, zone, test.expected) assert.Equal(t, modified, test.modified) }) } } func Test_addTxtEntryToZone(t *testing.T) { testCases := []struct { desc string input string expected string }{ { desc: "empty zone", input: "", expected: "_acme-challenge TXT 0 \"test\" 120", }, { desc: "zone with A entry", input: "@ A 0 192.0.2.2 3600", expected: "@ A 0 192.0.2.2 3600\n_acme-challenge TXT 0 \"test\" 120", }, { desc: "zone with required cleanup entry", input: "_acme-challenge TXT 0 \"old \" 120", expected: "_acme-challenge TXT 0 \"old\" 120\n_acme-challenge TXT 0 \"test\" 120", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { zone := AddTxtEntryToZone(test.input, "_acme-challenge", "test", 120) assert.Equal(t, zone, test.expected) }) } } func Test_fixTxtLines(t *testing.T) { testCases := []struct { desc string input string expected string }{ { desc: "clean-up", input: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE " 120`, expected: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`, }, { desc: "already cleaned", input: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`, expected: `_acme-challenge TXT 0 "SrqD25Gpm3WtIGKCqhgsLeXWE_FAD5Hv9CRoLAHxlIE" 120`, }, { desc: "special DNS entry", input: "$dyndns=yes:username:password", expected: "$dyndns=yes:username:password", }, { desc: "SRV entry", input: "_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300", expected: "_jabber._tcp SRV 20/0 xmpp-server1.l.google.com:5269 300", }, { desc: "MX entry", input: "@ MX 10 ASPMX.L.GOOGLE.COM 300", expected: "@ MX 10 ASPMX.L.GOOGLE.COM 300", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { line := fixTxtLines(test.input) assert.Equal(t, line, test.expected) }) } }