diff --git a/go.mod b/go.mod
index 52f4d2eb5..0a15d03c0 100644
--- a/go.mod
+++ b/go.mod
@@ -35,6 +35,7 @@ require (
github.com/exoscale/egoscale/v3 v3.1.13
github.com/go-jose/go-jose/v4 v4.0.5
github.com/go-viper/mapstructure/v2 v2.2.1
+ github.com/google/go-cmp v0.7.0
github.com/google/go-querystring v1.1.0
github.com/gophercloud/gophercloud v1.14.1
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56
diff --git a/platform/tester/servermock/builder.go b/platform/tester/servermock/builder.go
new file mode 100644
index 000000000..e3b41e5c3
--- /dev/null
+++ b/platform/tester/servermock/builder.go
@@ -0,0 +1,72 @@
+package servermock
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "slices"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+// Link represents a middleware interface, enabling middleware chaining.
+type Link interface {
+ Bind(next http.Handler) http.Handler
+}
+
+// LinkFunc defines a function type [Link].
+type LinkFunc func(next http.Handler) http.Handler
+
+func (f LinkFunc) Bind(next http.Handler) http.Handler {
+ return f(next)
+}
+
+// ClientBuilder defines a function type for creating a client of type T based on a httptest.Server instance.
+type ClientBuilder[T any] func(server *httptest.Server) (T, error)
+
+// Builder is a type that facilitates the construction of testable HTTP clients and server.
+// It allows defining routes, attaching middleware, and creating custom HTTP clients.
+type Builder[T any] struct {
+ mux *http.ServeMux
+ chain []Link
+
+ clientBuilder ClientBuilder[T]
+}
+
+func NewBuilder[T any](clientBuilder ClientBuilder[T], chain ...Link) *Builder[T] {
+ return &Builder[T]{
+ mux: http.NewServeMux(),
+ chain: chain,
+ clientBuilder: clientBuilder,
+ }
+}
+
+func (b *Builder[T]) Route(pattern string, handler http.Handler, chain ...Link) *Builder[T] {
+ if handler == nil {
+ handler = Noop()
+ }
+
+ for _, link := range slices.Backward(b.chain) {
+ handler = link.Bind(handler)
+ }
+
+ for _, link := range slices.Backward(chain) {
+ handler = link.Bind(handler)
+ }
+
+ b.mux.Handle(pattern, handler)
+
+ return b
+}
+
+func (b *Builder[T]) Build(t *testing.T) T {
+ t.Helper()
+
+ server := httptest.NewServer(b.mux)
+ t.Cleanup(server.Close)
+
+ client, err := b.clientBuilder(server)
+ require.NoError(t, err)
+
+ return client
+}
diff --git a/platform/tester/servermock/handler_dump.go b/platform/tester/servermock/handler_dump.go
new file mode 100644
index 000000000..83f902980
--- /dev/null
+++ b/platform/tester/servermock/handler_dump.go
@@ -0,0 +1,20 @@
+package servermock
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httputil"
+)
+
+// DumpRequest logs the full HTTP request to the console, including the body if present.
+func DumpRequest() http.HandlerFunc {
+ return func(rw http.ResponseWriter, req *http.Request) {
+ dump, err := httputil.DumpRequest(req, true)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ fmt.Println(string(dump))
+ }
+}
diff --git a/platform/tester/servermock/handler_file.go b/platform/tester/servermock/handler_file.go
new file mode 100644
index 000000000..d826c648a
--- /dev/null
+++ b/platform/tester/servermock/handler_file.go
@@ -0,0 +1,77 @@
+package servermock
+
+import (
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "slices"
+)
+
+// ResponseFromFileHandler handles HTTP responses using the content of a file.
+type ResponseFromFileHandler struct {
+ statusCode int
+ headers http.Header
+ filename string
+}
+
+func ResponseFromFile(filename string) *ResponseFromFileHandler {
+ return &ResponseFromFileHandler{
+ statusCode: http.StatusOK,
+ headers: http.Header{},
+ filename: filename,
+ }
+}
+
+func ResponseFromFixture(filename string) *ResponseFromFileHandler {
+ return ResponseFromFile(filepath.Join("fixtures", filename))
+}
+
+func (h *ResponseFromFileHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
+ for k, values := range h.headers {
+ for _, v := range values {
+ rw.Header().Add(k, v)
+ }
+ }
+
+ if h.filename == "" {
+ rw.WriteHeader(h.statusCode)
+ return
+ }
+
+ if filepath.Ext(h.filename) == ".json" {
+ rw.Header().Set(contentTypeHeader, applicationJSONMimeType)
+ }
+
+ file, err := os.Open(h.filename)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ defer func() { _ = file.Close() }()
+
+ rw.WriteHeader(h.statusCode)
+
+ _, err = io.Copy(rw, file)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (h *ResponseFromFileHandler) WithStatusCode(status int) *ResponseFromFileHandler {
+ if h.statusCode >= http.StatusContinue {
+ h.statusCode = status
+ }
+
+ return h
+}
+
+func (h *ResponseFromFileHandler) WithHeader(name, value string, values ...string) *ResponseFromFileHandler {
+ for _, v := range slices.Concat([]string{value}, values) {
+ h.headers.Add(name, v)
+ }
+
+ return h
+}
diff --git a/platform/tester/servermock/handler_json.go b/platform/tester/servermock/handler_json.go
new file mode 100644
index 000000000..f1c2aa9ce
--- /dev/null
+++ b/platform/tester/servermock/handler_json.go
@@ -0,0 +1,39 @@
+package servermock
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+// JSONEncodeHandler is a handler that encodes data into JSON and writes it to an HTTP response.
+type JSONEncodeHandler struct {
+ data any
+ statusCode int
+}
+
+func JSONEncode(data any) *JSONEncodeHandler {
+ return &JSONEncodeHandler{
+ data: data,
+ statusCode: http.StatusOK,
+ }
+}
+
+func (h *JSONEncodeHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
+ rw.Header().Set(contentTypeHeader, applicationJSONMimeType)
+
+ rw.WriteHeader(h.statusCode)
+
+ err := json.NewEncoder(rw).Encode(h.data)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (h *JSONEncodeHandler) WithStatusCode(status int) *JSONEncodeHandler {
+ if h.statusCode >= http.StatusContinue {
+ h.statusCode = status
+ }
+
+ return h
+}
diff --git a/platform/tester/servermock/handler_noop.go b/platform/tester/servermock/handler_noop.go
new file mode 100644
index 000000000..6df5164e6
--- /dev/null
+++ b/platform/tester/servermock/handler_noop.go
@@ -0,0 +1,45 @@
+package servermock
+
+import (
+ "net/http"
+ "slices"
+)
+
+// NoopHandler is a simple HTTP handler that responds without processing requests.
+type NoopHandler struct {
+ statusCode int
+ headers http.Header
+}
+
+func Noop() *NoopHandler {
+ return &NoopHandler{
+ statusCode: http.StatusOK,
+ headers: http.Header{},
+ }
+}
+
+func (h *NoopHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ for k, values := range h.headers {
+ for _, v := range values {
+ rw.Header().Add(k, v)
+ }
+ }
+
+ rw.WriteHeader(h.statusCode)
+}
+
+func (h *NoopHandler) WithStatusCode(status int) *NoopHandler {
+ if h.statusCode >= http.StatusContinue {
+ h.statusCode = status
+ }
+
+ return h
+}
+
+func (h *NoopHandler) WithHeader(name, value string, values ...string) *NoopHandler {
+ for _, v := range slices.Concat([]string{value}, values) {
+ h.headers.Add(name, v)
+ }
+
+ return h
+}
diff --git a/platform/tester/servermock/handler_raw.go b/platform/tester/servermock/handler_raw.go
new file mode 100644
index 000000000..d7c68f396
--- /dev/null
+++ b/platform/tester/servermock/handler_raw.go
@@ -0,0 +1,61 @@
+package servermock
+
+import (
+ "net/http"
+ "slices"
+)
+
+// RawResponseHandler is a custom HTTP handler that serves raw response data.
+type RawResponseHandler struct {
+ statusCode int
+ headers http.Header
+ data []byte
+}
+
+func RawResponse(data []byte) *RawResponseHandler {
+ return &RawResponseHandler{
+ statusCode: http.StatusOK,
+ headers: http.Header{},
+ data: data,
+ }
+}
+
+func RawStringResponse(data string) *RawResponseHandler {
+ return RawResponse([]byte(data))
+}
+
+func (h *RawResponseHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
+ for k, values := range h.headers {
+ for _, v := range values {
+ rw.Header().Add(k, v)
+ }
+ }
+
+ rw.WriteHeader(h.statusCode)
+
+ if len(h.data) == 0 {
+ return
+ }
+
+ _, err := rw.Write(h.data)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func (h *RawResponseHandler) WithStatusCode(status int) *RawResponseHandler {
+ if h.statusCode >= http.StatusContinue {
+ h.statusCode = status
+ }
+
+ return h
+}
+
+func (h *RawResponseHandler) WithHeader(name, value string, values ...string) *RawResponseHandler {
+ for _, v := range slices.Concat([]string{value}, values) {
+ h.headers.Add(name, v)
+ }
+
+ return h
+}
diff --git a/platform/tester/servermock/link_form.go b/platform/tester/servermock/link_form.go
new file mode 100644
index 000000000..e7541cefa
--- /dev/null
+++ b/platform/tester/servermock/link_form.go
@@ -0,0 +1,94 @@
+package servermock
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "regexp"
+ "slices"
+)
+
+// FormLink is a type used for validating and processing form data in HTTP requests.
+// It supports strict validation, predefined values, and regex-based checks to ensure form compliance.
+type FormLink struct {
+ values url.Values
+ regexes map[string]*regexp.Regexp
+ strict bool
+ usePostForm bool
+ statusCode int
+}
+
+func CheckForm() *FormLink {
+ return &FormLink{
+ values: url.Values{},
+ regexes: map[string]*regexp.Regexp{},
+ statusCode: http.StatusBadRequest,
+ }
+}
+
+func (l *FormLink) Bind(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ err := req.ParseForm()
+ if err != nil {
+ http.Error(rw, err.Error(), l.statusCode)
+ return
+ }
+
+ form := req.Form
+ if l.usePostForm {
+ form = req.PostForm
+ }
+
+ if l.strict {
+ if len(form) != len(l.values)+len(l.regexes) {
+ msg := fmt.Sprintf("invalid query parameters, got %v, want %v", req.Form, l.values)
+ http.Error(rw, msg, l.statusCode)
+ return
+ }
+ }
+
+ for k, v := range l.values {
+ value := form[k]
+ if !slices.Equal(v, value) {
+ msg := fmt.Sprintf("invalid %q form value, got %q, want %q", k, value, v)
+ http.Error(rw, msg, l.statusCode)
+ return
+ }
+ }
+
+ for k, exp := range l.regexes {
+ value := form.Get(k)
+ if !exp.MatchString(value) {
+ msg := fmt.Sprintf("invalid %q form value, %q doesn't match to %q", k, value, exp)
+ http.Error(rw, msg, l.statusCode)
+ return
+ }
+ }
+
+ next.ServeHTTP(rw, req)
+ })
+}
+
+func (l *FormLink) Strict() *FormLink {
+ l.strict = true
+
+ return l
+}
+
+func (l *FormLink) UsePostForm() *FormLink {
+ l.usePostForm = true
+
+ return l
+}
+
+func (l *FormLink) With(name, value string) *FormLink {
+ l.values.Set(name, value)
+
+ return l
+}
+
+func (l *FormLink) WithRegexp(name, exp string) *FormLink {
+ l.regexes[name] = regexp.MustCompile(exp)
+
+ return l
+}
diff --git a/platform/tester/servermock/link_headers.go b/platform/tester/servermock/link_headers.go
new file mode 100644
index 000000000..821c737fe
--- /dev/null
+++ b/platform/tester/servermock/link_headers.go
@@ -0,0 +1,177 @@
+package servermock
+
+import (
+ "fmt"
+ "net/http"
+ "regexp"
+ "slices"
+)
+
+const (
+ authorizationHeader = "Authorization"
+ contentTypeHeader = "Content-Type"
+ acceptHeader = "Accept"
+)
+
+const (
+ applicationJSONMimeType = "application/json"
+ applicationFormMimeType = "application/x-www-form-urlencoded"
+)
+
+type basicAuth struct {
+ username, password string
+}
+
+// HeaderLink validates HTTP request headers.
+type HeaderLink struct {
+ values http.Header
+ regexes map[string]*regexp.Regexp
+ json bool
+ basicAuth *basicAuth
+ statusCode int
+}
+
+func CheckHeader() *HeaderLink {
+ return &HeaderLink{
+ values: http.Header{},
+ regexes: map[string]*regexp.Regexp{},
+ statusCode: http.StatusBadRequest,
+ }
+}
+
+func (l *HeaderLink) Bind(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ for k, v := range l.values {
+ err := checkHeader(req, k, v)
+ if err != nil {
+ http.Error(rw, err.Error(), l.statusCode)
+ return
+ }
+ }
+
+ for k, exp := range l.regexes {
+ value := req.Header.Get(k)
+
+ if !exp.MatchString(value) {
+ msg := fmt.Sprintf("invalid %q header value, %q doesn't match to %q", k, value, exp)
+ http.Error(rw, msg, l.statusCode)
+ return
+ }
+ }
+
+ if l.json && !l.checkJSONHeaders(rw, req) {
+ return
+ }
+
+ if l.basicAuth != nil && !l.checkBasicAuth(rw, req) {
+ return
+ }
+
+ next.ServeHTTP(rw, req)
+ })
+}
+
+func (l *HeaderLink) With(name, value string, values ...string) *HeaderLink {
+ for _, v := range slices.Concat([]string{value}, values) {
+ l.values.Add(name, v)
+ }
+
+ return l
+}
+
+func (l *HeaderLink) WithRegexp(name, exp string) *HeaderLink {
+ l.regexes[name] = regexp.MustCompile(exp)
+
+ return l
+}
+
+func (l *HeaderLink) WithJSONHeaders() *HeaderLink {
+ l.json = true
+
+ return l
+}
+
+func (l *HeaderLink) WithContentTypeFromURLEncoded() *HeaderLink {
+ l.values.Set(contentTypeHeader, applicationFormMimeType)
+
+ return l
+}
+
+func (l *HeaderLink) WithContentType(value string) *HeaderLink {
+ l.values.Set(contentTypeHeader, value)
+
+ return l
+}
+
+func (l *HeaderLink) WithAccept(value string) *HeaderLink {
+ l.values.Set(acceptHeader, value)
+
+ return l
+}
+
+func (l *HeaderLink) WithAuthorization(value string) *HeaderLink {
+ l.values.Set(authorizationHeader, value)
+
+ return l
+}
+
+func (l *HeaderLink) WithStatusCode(status int) *HeaderLink {
+ if l.statusCode >= http.StatusContinue {
+ l.statusCode = status
+ }
+
+ return l
+}
+
+func (l *HeaderLink) WithBasicAuth(username, password string) *HeaderLink {
+ l.basicAuth = &basicAuth{username: username, password: password}
+
+ return l
+}
+
+func (l *HeaderLink) checkBasicAuth(rw http.ResponseWriter, req *http.Request) bool {
+ usr, pwd, ok := req.BasicAuth()
+ if !ok {
+ http.Error(rw, "missing Basic auth", l.statusCode)
+
+ return false
+ }
+
+ if usr != l.basicAuth.username || pwd != l.basicAuth.password {
+ msg := fmt.Sprintf("invalid credentials: got [username: %q, password: %q], want [username: %q, password: %q]",
+ usr, pwd, l.basicAuth.username, l.basicAuth.password)
+ http.Error(rw, msg, l.statusCode)
+
+ return false
+ }
+
+ return true
+}
+
+func (l *HeaderLink) checkJSONHeaders(rw http.ResponseWriter, req *http.Request) bool {
+ err := checkHeader(req, acceptHeader, []string{applicationJSONMimeType})
+ if err != nil {
+ http.Error(rw, err.Error(), l.statusCode)
+
+ return false
+ }
+
+ if req.ContentLength > 0 {
+ err = checkHeader(req, contentTypeHeader, []string{applicationJSONMimeType})
+ if err != nil {
+ http.Error(rw, err.Error(), l.statusCode)
+
+ return false
+ }
+ }
+
+ return true
+}
+
+func checkHeader(req *http.Request, k string, v []string) error {
+ if !slices.Equal(req.Header[k], v) {
+ return fmt.Errorf("invalid %q header value, got %q, want %q", k, req.Header[k], v)
+ }
+
+ return nil
+}
diff --git a/platform/tester/servermock/link_query.go b/platform/tester/servermock/link_query.go
new file mode 100644
index 000000000..00d7450ae
--- /dev/null
+++ b/platform/tester/servermock/link_query.go
@@ -0,0 +1,97 @@
+package servermock
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "regexp"
+)
+
+// QueryParameterLink validates query parameters in HTTP requests.
+// The strict flag enforces exact matches with specified query parameters.
+type QueryParameterLink struct {
+ values map[string]string
+ regexes map[string]*regexp.Regexp
+ strict bool
+ statusCode int
+}
+
+func CheckQueryParameter() *QueryParameterLink {
+ return &QueryParameterLink{
+ values: map[string]string{},
+ regexes: map[string]*regexp.Regexp{},
+ statusCode: http.StatusBadRequest,
+ }
+}
+
+func (l *QueryParameterLink) Bind(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ query := req.URL.Query()
+
+ if l.strict {
+ if len(query) != len(l.values)+len(l.regexes) {
+ msg := fmt.Sprintf("invalid query parameters, got %v, want %v", query, l.values)
+ http.Error(rw, msg, l.statusCode)
+ return
+ }
+ }
+
+ for k, v := range l.values {
+ p := query.Get(k)
+ if p != v {
+ msg := fmt.Sprintf("invalid %q query parameter value, got %q, want %q", k, p, v)
+ http.Error(rw, msg, l.statusCode)
+ return
+ }
+ }
+
+ for k, exp := range l.regexes {
+ value := query.Get(k)
+ if !exp.MatchString(value) {
+ msg := fmt.Sprintf("invalid %q query parameter value, %q doesn't match to %q", k, value, exp)
+ http.Error(rw, msg, l.statusCode)
+ return
+ }
+ }
+
+ next.ServeHTTP(rw, req)
+ })
+}
+
+func (l *QueryParameterLink) Strict() *QueryParameterLink {
+ l.strict = true
+
+ return l
+}
+
+func (l *QueryParameterLink) With(name, value string) *QueryParameterLink {
+ l.values[name] = value
+
+ return l
+}
+
+func (l *QueryParameterLink) WithRegexp(name, exp string) *QueryParameterLink {
+ l.regexes[name] = regexp.MustCompile(exp)
+
+ return l
+}
+
+func (l *QueryParameterLink) WithValues(values url.Values) *QueryParameterLink {
+ for k, v := range values {
+ if len(v) != 1 {
+ continue
+ }
+
+ l.values[k] = v[0]
+ }
+
+ return l
+}
+
+func (l *QueryParameterLink) WithStatusCode(status int) *QueryParameterLink {
+ if l.statusCode >= http.StatusContinue {
+ l.statusCode = status
+ }
+
+ return l
+}
diff --git a/platform/tester/servermock/link_request_body.go b/platform/tester/servermock/link_request_body.go
new file mode 100644
index 000000000..67ab4ae3f
--- /dev/null
+++ b/platform/tester/servermock/link_request_body.go
@@ -0,0 +1,89 @@
+package servermock
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "slices"
+)
+
+// RequestBodyLink represents a handler utility to validate HTTP request bodies against a predefined byte slice.
+type RequestBodyLink struct {
+ body []byte
+ filename string
+ ignoreWhitespace bool
+}
+
+// CheckRequestBody creates a [RequestBodyLink] initialized with the provided request body string.
+func CheckRequestBody(body string) *RequestBodyLink {
+ return &RequestBodyLink{body: []byte(body)}
+}
+
+// CheckRequestBodyFromFile creates a [RequestBodyLink] initialized with the provided request body file.
+func CheckRequestBodyFromFile(filename string) *RequestBodyLink {
+ return &RequestBodyLink{filename: filename}
+}
+
+func (l *RequestBodyLink) Bind(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ if req.ContentLength == 0 {
+ http.Error(rw, fmt.Sprintf("%s: empty request body", req.URL.Path), http.StatusBadRequest)
+ return
+ }
+
+ body, err := io.ReadAll(req.Body)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ _ = req.Body.Close()
+
+ expectedRaw := slices.Clone(l.body)
+
+ if l.filename != "" {
+ expectedRaw, err = os.ReadFile(filepath.Join("fixtures", l.filename))
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+
+ if len(expectedRaw) == 0 {
+ http.Error(rw, fmt.Sprintf("%s: empty expected request body", req.URL.Path), http.StatusBadRequest)
+ return
+ }
+
+ if l.ignoreWhitespace {
+ body = trimLineSpace(body)
+ expectedRaw = trimLineSpace(expectedRaw)
+ }
+
+ if !bytes.Equal(bytes.TrimSpace(expectedRaw), bytes.TrimSpace(body)) {
+ msg := fmt.Sprintf("%s: request body differences: got: %s, want: %s", req.URL.Path,
+ string(bytes.TrimSpace(body)), string(bytes.TrimSpace(expectedRaw)))
+ http.Error(rw, msg, http.StatusBadRequest)
+ return
+ }
+
+ next.ServeHTTP(rw, req)
+ })
+}
+
+func (l *RequestBodyLink) IgnoreWhitespace() *RequestBodyLink {
+ l.ignoreWhitespace = true
+
+ return l
+}
+
+func trimLineSpace(body []byte) []byte {
+ buf := bytes.NewBuffer(nil)
+ for line := range bytes.Lines(body) {
+ buf.Write(bytes.TrimSpace(line))
+ }
+
+ return buf.Bytes()
+}
diff --git a/platform/tester/servermock/link_request_body_json.go b/platform/tester/servermock/link_request_body_json.go
new file mode 100644
index 000000000..1d1fecce9
--- /dev/null
+++ b/platform/tester/servermock/link_request_body_json.go
@@ -0,0 +1,99 @@
+package servermock
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "slices"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+// RequestBodyJSONLink validates JSON request bodies.
+type RequestBodyJSONLink struct {
+ body []byte
+ filename string
+ data any
+}
+
+// CheckRequestJSONBody creates a [RequestBodyJSONLink] initialized with a string.
+func CheckRequestJSONBody(body string) *RequestBodyJSONLink {
+ return &RequestBodyJSONLink{body: []byte(body)}
+}
+
+// CheckRequestJSONBodyFromStruct creates a [RequestBodyJSONLink] initialized with a struct.
+func CheckRequestJSONBodyFromStruct(data any) *RequestBodyJSONLink {
+ return &RequestBodyJSONLink{data: data}
+}
+
+// CheckRequestJSONBodyFromFile creates a [RequestBodyJSONLink] initialized with the provided request body file.
+func CheckRequestJSONBodyFromFile(filename string) *RequestBodyJSONLink {
+ return &RequestBodyJSONLink{filename: filename}
+}
+
+func (l *RequestBodyJSONLink) Bind(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ if req.ContentLength == 0 {
+ http.Error(rw, fmt.Sprintf("%s: empty request body", req.URL.Path), http.StatusBadRequest)
+ return
+ }
+
+ body, err := io.ReadAll(req.Body)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ _ = req.Body.Close()
+
+ var expected, actual any
+
+ expectedRaw := slices.Clone(l.body)
+
+ switch {
+ case l.filename != "":
+ expectedRaw, err = os.ReadFile(filepath.Join("fixtures", l.filename))
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ case l.data != nil:
+ expectedRaw, err = json.Marshal(l.data)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+
+ if len(expectedRaw) == 0 {
+ http.Error(rw, fmt.Sprintf("%s: empty expected request body", req.URL.Path), http.StatusBadRequest)
+ return
+ }
+
+ err = json.Unmarshal(expectedRaw, &expected)
+ if err != nil {
+ msg := fmt.Sprintf("%s: the expected request body is not valid JSON: %v", req.URL.Path, err)
+ http.Error(rw, msg, http.StatusBadRequest)
+ return
+ }
+
+ err = json.Unmarshal(body, &actual)
+ if err != nil {
+ msg := fmt.Sprintf("%s: request body is not valid JSON: %v", req.URL.Path, err)
+ http.Error(rw, msg, http.StatusBadRequest)
+ return
+ }
+
+ if !cmp.Equal(actual, expected) {
+ msg := fmt.Sprintf("%s: request body differences: %s", req.URL.Path, cmp.Diff(actual, expected))
+ http.Error(rw, msg, http.StatusBadRequest)
+ return
+ }
+
+ next.ServeHTTP(rw, req)
+ })
+}
diff --git a/providers/dns/acmedns/acmedns_test.go b/providers/dns/acmedns/acmedns_test.go
index 3bc847b6d..e50c89d56 100644
--- a/providers/dns/acmedns/acmedns_test.go
+++ b/providers/dns/acmedns/acmedns_test.go
@@ -5,6 +5,7 @@ import (
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/nrdcg/goacmedns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -166,11 +167,17 @@ func TestPresent_httpStorage(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
+ config := servermock.NewBuilder(func(server *httptest.Server) (*Config, error) {
+ cfg := NewDefaultConfig()
+ cfg.StorageBaseURL = server.URL
- config := NewDefaultConfig()
- config.StorageBaseURL = server.URL
+ return cfg, nil
+ }).
+ // Fetch
+ Route("GET /example.com", servermock.Noop().WithStatusCode(http.StatusNotFound)).
+ // Put
+ Route("POST /example.com", servermock.Noop().WithStatusCode(test.StatusCode)).
+ Build(t)
p, err := NewDNSProviderConfig(config)
require.NoError(t, err)
@@ -178,16 +185,6 @@ func TestPresent_httpStorage(t *testing.T) {
client := newMockClient().WithRegisterAccount(egTestAccount)
p.client = client
- // Fetch
- mux.HandleFunc("GET /example.com", func(rw http.ResponseWriter, reg *http.Request) {
- rw.WriteHeader(http.StatusNotFound)
- })
-
- // Put
- mux.HandleFunc("POST /example.com", func(rw http.ResponseWriter, req *http.Request) {
- rw.WriteHeader(test.StatusCode)
- })
-
err = p.Present(egDomain, "foo", egKeyAuth)
if test.ExpectedError != nil {
assert.Equal(t, test.ExpectedError, err)
@@ -225,22 +222,21 @@ func TestRegister_httpStorage(t *testing.T) {
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
+ config := servermock.NewBuilder(func(server *httptest.Server) (*Config, error) {
+ cfg := NewDefaultConfig()
+ cfg.StorageBaseURL = server.URL
- config := NewDefaultConfig()
- config.StorageBaseURL = server.URL
+ return cfg, nil
+ }).
+ // Put
+ Route("POST /example.com", servermock.Noop().WithStatusCode(test.StatusCode)).
+ Build(t)
p, err := NewDNSProviderConfig(config)
require.NoError(t, err)
p.client = newMockClient().WithRegisterAccount(egTestAccount)
- // Put
- mux.HandleFunc("POST /example.com", func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(test.StatusCode)
- })
-
acc, err := p.register(t.Context(), egDomain, egFQDN)
if test.ExpectedError != nil {
assert.Equal(t, test.ExpectedError, err)
diff --git a/providers/dns/acmedns/internal/fixtures/request-body.json b/providers/dns/acmedns/internal/fixtures/request-body.json
new file mode 100644
index 000000000..d29cebc5b
--- /dev/null
+++ b/providers/dns/acmedns/internal/fixtures/request-body.json
@@ -0,0 +1,7 @@
+{
+ "fulldomain": "foo.example.com",
+ "subdomain": "foo",
+ "username": "user",
+ "password": "secret",
+ "server_url": "https://example.com"
+}
diff --git a/providers/dns/acmedns/internal/http_storage_test.go b/providers/dns/acmedns/internal/http_storage_test.go
index 14a5fd97c..0be6dd949 100644
--- a/providers/dns/acmedns/internal/http_storage_test.go
+++ b/providers/dns/acmedns/internal/http_storage_test.go
@@ -1,57 +1,35 @@
package internal
import (
- "io"
"net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/nrdcg/goacmedns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern, filename string, statusCode int) *HTTPStorage {
- t.Helper()
+func mockBuilder() *servermock.Builder[*HTTPStorage] {
+ return servermock.NewBuilder[*HTTPStorage](
+ func(server *httptest.Server) (*HTTPStorage, error) {
+ storage, err := NewHTTPStorage(server.URL)
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ storage.client = server.Client()
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(statusCode)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- storage, err := NewHTTPStorage(server.URL)
- require.NoError(t, err)
-
- storage.client = server.Client()
-
- return storage
+ return storage, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders())
}
func TestHTTPStorage_Fetch(t *testing.T) {
- storage := setupTest(t, "GET /example.com", "fetch.json", http.StatusOK)
+ storage := mockBuilder().
+ Route("GET /example.com", servermock.ResponseFromFixture("fetch.json")).
+ Build(t)
account, err := storage.Fetch(t.Context(), "example.com")
require.NoError(t, err)
@@ -68,14 +46,20 @@ func TestHTTPStorage_Fetch(t *testing.T) {
}
func TestHTTPStorage_Fetch_error(t *testing.T) {
- storage := setupTest(t, "GET /example.com", "error.json", http.StatusInternalServerError)
+ storage := mockBuilder().
+ Route("GET /example.com",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
_, err := storage.Fetch(t.Context(), "example.com")
require.Error(t, err)
}
func TestHTTPStorage_FetchAll(t *testing.T) {
- storage := setupTest(t, "GET /", "fetch-all.json", http.StatusOK)
+ storage := mockBuilder().
+ Route("GET /", servermock.ResponseFromFixture("fetch-all.json")).
+ Build(t)
account, err := storage.FetchAll(t.Context())
require.NoError(t, err)
@@ -101,14 +85,21 @@ func TestHTTPStorage_FetchAll(t *testing.T) {
}
func TestHTTPStorage_FetchAll_error(t *testing.T) {
- storage := setupTest(t, "GET /", "error.json", http.StatusInternalServerError)
+ storage := mockBuilder().
+ Route("GET /",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
_, err := storage.FetchAll(t.Context())
require.Error(t, err)
}
func TestHTTPStorage_Put(t *testing.T) {
- storage := setupTest(t, "POST /example.com", "", http.StatusOK)
+ storage := mockBuilder().
+ Route("POST /example.com", nil,
+ servermock.CheckRequestJSONBodyFromFile("request-body.json")).
+ Build(t)
account := goacmedns.Account{
FullDomain: "foo.example.com",
@@ -123,7 +114,11 @@ func TestHTTPStorage_Put(t *testing.T) {
}
func TestHTTPStorage_Put_error(t *testing.T) {
- storage := setupTest(t, "POST /example.com", "error.json", http.StatusInternalServerError)
+ storage := mockBuilder().
+ Route("POST /example.com",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
account := goacmedns.Account{
FullDomain: "foo.example.com",
@@ -138,7 +133,12 @@ func TestHTTPStorage_Put_error(t *testing.T) {
}
func TestHTTPStorage_Put_CNAME_created(t *testing.T) {
- storage := setupTest(t, "POST /example.com", "", http.StatusCreated)
+ storage := mockBuilder().
+ Route("POST /example.com",
+ servermock.Noop().
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBodyFromFile("request-body.json")).
+ Build(t)
account := goacmedns.Account{
FullDomain: "foo.example.com",
diff --git a/providers/dns/allinkl/internal/client_test.go b/providers/dns/allinkl/internal/client_test.go
index 6ccb1ebf6..5954e2463 100644
--- a/providers/dns/allinkl/internal/client_test.go
+++ b/providers/dns/allinkl/internal/client_test.go
@@ -1,27 +1,28 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestClient_GetDNSSettings(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", testHandler("get_dns_settings.xml"))
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("user")
client.baseURL = server.URL
+ client.HTTPClient = server.Client()
+
+ return client, nil
+}
+
+func TestClient_GetDNSSettings(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /", servermock.ResponseFromFixture("get_dns_settings.xml"),
+ servermock.CheckRequestBodyFromFile("get_dns_settings-request.xml").
+ IgnoreWhitespace()).
+ Build(t)
records, err := client.GetDNSSettings(mockContext(t), "example.com", "")
require.NoError(t, err)
@@ -96,14 +97,11 @@ func TestClient_GetDNSSettings(t *testing.T) {
}
func TestClient_AddDNSSettings(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", testHandler("add_dns_settings.xml"))
-
- client := NewClient("user")
- client.baseURL = server.URL
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /", servermock.ResponseFromFixture("add_dns_settings.xml"),
+ servermock.CheckRequestBodyFromFile("add_dns_settings-request.xml").
+ IgnoreWhitespace()).
+ Build(t)
record := DNSRequest{
ZoneHost: "42cnc.de.",
@@ -119,40 +117,14 @@ func TestClient_AddDNSSettings(t *testing.T) {
}
func TestClient_DeleteDNSSettings(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", testHandler("delete_dns_settings.xml"))
-
- client := NewClient("user")
- client.baseURL = server.URL
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /", servermock.ResponseFromFixture("delete_dns_settings.xml"),
+ servermock.CheckRequestBodyFromFile("delete_dns_settings-request.xml").
+ IgnoreWhitespace()).
+ Build(t)
r, err := client.DeleteDNSSettings(mockContext(t), "57347450")
require.NoError(t, err)
assert.Equal(t, "TRUE", r)
}
-
-func testHandler(filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
diff --git a/providers/dns/allinkl/internal/fixtures/add_dns_settings-request.xml b/providers/dns/allinkl/internal/fixtures/add_dns_settings-request.xml
new file mode 100644
index 000000000..e8cd12633
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/add_dns_settings-request.xml
@@ -0,0 +1,7 @@
+
+
+
+ {"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"add_dns_settings","KasRequestParams":{"zone_host":"42cnc.de.","record_type":"TXT","record_name":"lego","record_data":"abcdefgh","record_aux":0}}
+
+
+
diff --git a/providers/dns/allinkl/internal/fixtures/delete_dns_settings-request.xml b/providers/dns/allinkl/internal/fixtures/delete_dns_settings-request.xml
new file mode 100644
index 000000000..a306a98a7
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/delete_dns_settings-request.xml
@@ -0,0 +1,7 @@
+
+
+
+ {"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"delete_dns_settings","KasRequestParams":{"record_id":"57347450"}}
+
+
+
diff --git a/providers/dns/allinkl/internal/fixtures/get_dns_settings-request.xml b/providers/dns/allinkl/internal/fixtures/get_dns_settings-request.xml
new file mode 100644
index 000000000..b44941d2b
--- /dev/null
+++ b/providers/dns/allinkl/internal/fixtures/get_dns_settings-request.xml
@@ -0,0 +1,7 @@
+
+
+
+ {"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"get_dns_settings","KasRequestParams":{"zone_host":"example.com"}}
+
+
+
diff --git a/providers/dns/allinkl/internal/identity_test.go b/providers/dns/allinkl/internal/identity_test.go
index 2ef0a4ca4..dc55506f2 100644
--- a/providers/dns/allinkl/internal/identity_test.go
+++ b/providers/dns/allinkl/internal/identity_test.go
@@ -2,14 +2,21 @@ package internal
import (
"context"
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+func setupIdentifierClient(server *httptest.Server) (*Identifier, error) {
+ client := NewIdentifier("user", "secret")
+ client.authEndpoint = server.URL
+
+ return client, nil
+}
+
func mockContext(t *testing.T) context.Context {
t.Helper()
@@ -17,14 +24,9 @@ func mockContext(t *testing.T) context.Context {
}
func TestIdentifier_Authentication(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", testHandler("auth.xml"))
-
- client := NewIdentifier("user", "secret")
- client.authEndpoint = server.URL
+ client := servermock.NewBuilder[*Identifier](setupIdentifierClient).
+ Route("POST /", servermock.ResponseFromFixture("auth.xml")).
+ Build(t)
credentialToken, err := client.Authentication(t.Context(), 60, false)
require.NoError(t, err)
@@ -33,14 +35,9 @@ func TestIdentifier_Authentication(t *testing.T) {
}
func TestIdentifier_Authentication_error(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", testHandler("auth_fault.xml"))
-
- client := NewIdentifier("user", "secret")
- client.authEndpoint = server.URL
+ client := servermock.NewBuilder[*Identifier](setupIdentifierClient).
+ Route("POST /", servermock.ResponseFromFixture("auth_fault.xml")).
+ Build(t)
_, err := client.Authentication(t.Context(), 60, false)
require.Error(t, err)
diff --git a/providers/dns/arvancloud/internal/client_test.go b/providers/dns/arvancloud/internal/client_test.go
index 2930dcb3d..38cb740c1 100644
--- a/providers/dns/arvancloud/internal/client_test.go
+++ b/providers/dns/arvancloud/internal/client_test.go
@@ -1,64 +1,39 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, apiKey string) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder(apiKey string) *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(apiKey)
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(apiKey)
- client.baseURL, _ = url.Parse(server.URL)
- client.HTTPClient = server.Client()
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization(apiKey))
}
func TestClient_GetTxtRecord(t *testing.T) {
const apiKey = "myKeyA"
- client, mux := setupTest(t, apiKey)
-
const domain = "example.com"
- mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != apiKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open("./fixtures/get_txt_record.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder(apiKey).
+ Route("GET /cdn/4.0/domains/"+domain+"/dns-records",
+ servermock.ResponseFromFixture("get_txt_record.json"),
+ servermock.CheckQueryParameter().With("search", "acme-challenge")).
+ Build(t)
_, err := client.GetTxtRecord(t.Context(), domain, "_acme-challenge", "txtxtxt")
require.NoError(t, err)
@@ -67,36 +42,14 @@ func TestClient_GetTxtRecord(t *testing.T) {
func TestClient_CreateRecord(t *testing.T) {
const apiKey = "myKeyB"
- client, mux := setupTest(t, apiKey)
-
const domain = "example.com"
- mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != apiKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open("./fixtures/create_txt_record.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(http.StatusCreated)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder(apiKey).
+ Route("POST /cdn/4.0/domains/"+domain+"/dns-records",
+ servermock.ResponseFromFixture("create_txt_record.json").
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBodyFromFile("create_record-request.json")).
+ Build(t)
record := DNSRecord{
Name: "_acme-challenge",
@@ -128,23 +81,12 @@ func TestClient_CreateRecord(t *testing.T) {
func TestClient_DeleteRecord(t *testing.T) {
const apiKey = "myKeyC"
- client, mux := setupTest(t, apiKey)
-
const domain = "example.com"
const recordID = "recordId"
- mux.HandleFunc("/cdn/4.0/domains/"+domain+"/dns-records/"+recordID, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != apiKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
- })
+ client := mockBuilder(apiKey).
+ Route("DELETE /cdn/4.0/domains/"+domain+"/dns-records/"+recordID, nil).
+ Build(t)
err := client.DeleteRecord(t.Context(), domain, recordID)
require.NoError(t, err)
diff --git a/providers/dns/arvancloud/internal/fixtures/create_record-request.json b/providers/dns/arvancloud/internal/fixtures/create_record-request.json
new file mode 100644
index 000000000..48a7124f6
--- /dev/null
+++ b/providers/dns/arvancloud/internal/fixtures/create_record-request.json
@@ -0,0 +1,8 @@
+{
+ "type": "txt",
+ "value": {
+ "text": "txtxtxt"
+ },
+ "name": "_acme-challenge",
+ "ttl": 600
+}
diff --git a/providers/dns/auroradns/auroradns_test.go b/providers/dns/auroradns/auroradns_test.go
index cbd51b830..1619ee586 100644
--- a/providers/dns/auroradns/auroradns_test.go
+++ b/providers/dns/auroradns/auroradns_test.go
@@ -1,35 +1,32 @@
package auroradns
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
- "github.com/stretchr/testify/assert"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
+ "github.com/nrdcg/auroradns"
"github.com/stretchr/testify/require"
)
var envTest = tester.NewEnvTest(EnvAPIKey, EnvSecret)
-func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.APIKey = "asdf1234"
+ config.Secret = "key"
+ config.BaseURL = server.URL
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- config := NewDefaultConfig()
- config.APIKey = "asdf1234"
- config.Secret = "key"
- config.BaseURL = server.URL
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- return provider, mux
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().
+ WithContentType("application/json").
+ WithRegexp("Authorization", `AuroraDNSv1 .+`).
+ WithRegexp("X-Auroradns-Date", `[0-9TZ]+`))
}
func TestNewDNSProvider(t *testing.T) {
@@ -145,72 +142,47 @@ func TestNewDNSProviderConfig(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
-
- w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `[{
- "id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
- "name": "example.com"
- }]`)
- })
-
- mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodPost, r.Method)
- assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type")
-
- reqBody, err := io.ReadAll(r.Body)
- require.NoError(t, err)
- assert.JSONEq(t, `{"type":"TXT","name":"_acme-challenge","content":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":300}`, string(reqBody))
-
- w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `{
- "id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
- "type": "TXT",
- "name": "_acme-challenge",
- "ttl": 300
- }`)
- })
+ provider := mockBuilder().
+ Route("GET /zones",
+ servermock.JSONEncode([]auroradns.Zone{{
+ ID: "c56a4180-65aa-42ec-a945-5fd21dec0538",
+ Name: "example.com",
+ }}).
+ WithStatusCode(http.StatusCreated)).
+ Route("POST /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records",
+ servermock.JSONEncode(auroradns.Record{
+ ID: "ec56a4180-65aa-42ec-a945-5fd21dec0538",
+ RecordType: "TXT",
+ Name: "_acme-challenge",
+ TTL: 300,
+ }).
+ WithStatusCode(http.StatusCreated)).
+ Build(t)
err := provider.Present("example.com", "", "foobar")
require.NoError(t, err, "fail to create TXT record")
}
func TestDNSProvider_CleanUp(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method)
-
- w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `[{
- "id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
- "name": "example.com"
- }]`)
- })
-
- mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodPost, r.Method)
-
- w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `{
- "id": "ec56a4180-65aa-42ec-a945-5fd21dec0538",
- "type": "TXT",
- "name": "_acme-challenge",
- "ttl": 300
- }`)
- })
-
- mux.HandleFunc("/zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records/ec56a4180-65aa-42ec-a945-5fd21dec0538", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodDelete, r.Method)
-
- assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type")
-
- w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `{}`)
- })
+ provider := mockBuilder().
+ Route("GET /zones",
+ servermock.JSONEncode([]auroradns.Zone{{
+ ID: "c56a4180-65aa-42ec-a945-5fd21dec0538",
+ Name: "example.com",
+ }}).
+ WithStatusCode(http.StatusCreated)).
+ Route("POST /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records",
+ servermock.JSONEncode(auroradns.Record{
+ ID: "ec56a4180-65aa-42ec-a945-5fd21dec0538",
+ RecordType: "TXT",
+ Name: "_acme-challenge",
+ TTL: 300,
+ }).
+ WithStatusCode(http.StatusCreated)).
+ Route("DELETE /zones/c56a4180-65aa-42ec-a945-5fd21dec0538/records/ec56a4180-65aa-42ec-a945-5fd21dec0538",
+ servermock.RawStringResponse("{}").
+ WithStatusCode(http.StatusCreated)).
+ Build(t)
err := provider.Present("example.com", "", "foobar")
require.NoError(t, err, "fail to create TXT record")
diff --git a/providers/dns/autodns/internal/client_test.go b/providers/dns/autodns/internal/client_test.go
index d656e0ae9..5eb6486ea 100644
--- a/providers/dns/autodns/internal/client_test.go
+++ b/providers/dns/autodns/internal/client_test.go
@@ -1,68 +1,37 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret", 123)
+ client.HTTPClient = server.Client()
+ client.BaseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- apiUser, apiKey, ok := req.BasicAuth()
- if apiUser != "user" || apiKey != "secret" || !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client := NewClient("user", "secret", 123)
- client.HTTPClient = server.Client()
- client.BaseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithBasicAuth("user", "secret").
+ WithJSONHeaders())
}
func TestClient_AddTxtRecords(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/zone/example.com/_stream", http.StatusOK, "add-record.json")
+ client := mockBuilder().
+ Route("POST /zone/example.com/_stream",
+ servermock.ResponseFromFixture("add_record.json"),
+ servermock.CheckRequestJSONBodyFromFile("add_record-request.json"),
+ servermock.CheckHeader().
+ With("X-Domainrobot-Context", "123")).
+ Build(t)
records := []*ResourceRecord{{}}
@@ -86,7 +55,13 @@ func TestClient_AddTxtRecords(t *testing.T) {
}
func TestClient_RemoveTXTRecords(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/zone/example.com/_stream", http.StatusOK, "add-record.json")
+ client := mockBuilder().
+ Route("POST /zone/example.com/_stream",
+ servermock.ResponseFromFixture("remove_record.json"),
+ servermock.CheckRequestJSONBodyFromFile("remove_record-request.json"),
+ servermock.CheckHeader().
+ With("X-Domainrobot-Context", "123")).
+ Build(t)
records := []*ResourceRecord{{}}
diff --git a/providers/dns/autodns/internal/fixtures/add_record-request.json b/providers/dns/autodns/internal/fixtures/add_record-request.json
new file mode 100644
index 000000000..b798b4fbd
--- /dev/null
+++ b/providers/dns/autodns/internal/fixtures/add_record-request.json
@@ -0,0 +1,11 @@
+{
+ "adds": [
+ {
+ "name": "",
+ "ttl": 0,
+ "type": "",
+ "value": ""
+ }
+ ],
+ "rems": null
+}
diff --git a/providers/dns/autodns/internal/fixtures/add-record.json b/providers/dns/autodns/internal/fixtures/add_record.json
similarity index 100%
rename from providers/dns/autodns/internal/fixtures/add-record.json
rename to providers/dns/autodns/internal/fixtures/add_record.json
diff --git a/providers/dns/autodns/internal/fixtures/remove_record-request.json b/providers/dns/autodns/internal/fixtures/remove_record-request.json
new file mode 100644
index 000000000..0702c7367
--- /dev/null
+++ b/providers/dns/autodns/internal/fixtures/remove_record-request.json
@@ -0,0 +1,11 @@
+{
+ "adds": null,
+ "rems": [
+ {
+ "name": "",
+ "ttl": 0,
+ "type": "",
+ "value": ""
+ }
+ ]
+}
diff --git a/providers/dns/autodns/internal/fixtures/remove-record.json b/providers/dns/autodns/internal/fixtures/remove_record.json
similarity index 100%
rename from providers/dns/autodns/internal/fixtures/remove-record.json
rename to providers/dns/autodns/internal/fixtures/remove_record.json
diff --git a/providers/dns/axelname/internal/client_test.go b/providers/dns/axelname/internal/client_test.go
index 0ead4b180..7796f6047 100644
--- a/providers/dns/axelname/internal/client_test.go
+++ b/providers/dns/axelname/internal/client_test.go
@@ -1,58 +1,37 @@
package internal
import (
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
+func setupClient(server *httptest.Server) (*Client, error) {
client, err := NewClient("user", "secret")
- require.NoError(t, err)
+ if err != nil {
+ return nil, err
+ }
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
- return client
+ return client, nil
}
func TestClient_ListRecords(t *testing.T) {
- client := setupTest(t, "GET /dns_list", http.StatusOK, "dns_list.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /dns_list",
+ servermock.ResponseFromFixture("dns_list.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("domain", "example.com").
+ With("nichdl", "user").
+ With("token", "secret")).
+ Build(t)
records, err := client.ListRecords(t.Context(), "example.com")
require.NoError(t, err)
@@ -68,14 +47,26 @@ func TestClient_ListRecords(t *testing.T) {
}
func TestClient_ListRecords_error(t *testing.T) {
- client := setupTest(t, "GET /dns_list", http.StatusNotFound, "dns_list_error.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /dns_list",
+ servermock.ResponseFromFixture("dns_list_error.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
_, err := client.ListRecords(t.Context(), "example.com")
require.EqualError(t, err, "error: Domain not found (1)")
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "GET /dns_delete", http.StatusOK, "dns_delete.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /dns_delete",
+ servermock.ResponseFromFixture("dns_delete.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("id", "74749").
+ With("domain", "example.com").
+ With("nichdl", "user").
+ With("token", "secret")).
+ Build(t)
record := Record{ID: "74749"}
@@ -84,7 +75,11 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "GET /dns_delete", http.StatusNotFound, "dns_delete_error.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /dns_delete",
+ servermock.ResponseFromFixture("dns_delete_error.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
record := Record{ID: "74749"}
@@ -93,7 +88,15 @@ func TestClient_DeleteRecord_error(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "GET /dns_add", http.StatusOK, "dns_add.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /dns_add",
+ servermock.ResponseFromFixture("dns_add.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("id", "74749").
+ With("domain", "example.com").
+ With("nichdl", "user").
+ With("token", "secret")).
+ Build(t)
record := Record{ID: "74749"}
@@ -102,7 +105,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "GET /dns_add", http.StatusNotFound, "dns_add_error.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /dns_add",
+ servermock.ResponseFromFixture("dns_add_error.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
record := Record{ID: "74749"}
diff --git a/providers/dns/azion/azion_test.go b/providers/dns/azion/azion_test.go
index de25e7c69..b3b553114 100644
--- a/providers/dns/azion/azion_test.go
+++ b/providers/dns/azion/azion_test.go
@@ -2,15 +2,12 @@ package azion
import (
"context"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
"github.com/aziontech/azionapi-go-sdk/idns"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -123,8 +120,9 @@ func TestLiveCleanUp(t *testing.T) {
}
func TestDNSProvider_findZone(t *testing.T) {
- provider, mux := setupTest(t)
- mux.HandleFunc("GET /intelligent_dns", writeFixtureHandler("zones.json"))
+ provider := mockBuilder().
+ Route("GET /intelligent_dns", servermock.ResponseFromFixture("zones.json")).
+ Build(t)
testCases := []struct {
desc string
@@ -198,8 +196,9 @@ func TestDNSProvider_findZone_error(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- provider, mux := setupTest(t)
- mux.HandleFunc("GET /intelligent_dns", writeFixtureHandler(test.response))
+ provider := mockBuilder().
+ Route("GET /intelligent_dns", servermock.ResponseFromFixture(test.response)).
+ Build(t)
zone, err := provider.findZone(context.Background(), test.fqdn)
require.EqualError(t, err, test.expected)
@@ -209,41 +208,25 @@ func TestDNSProvider_findZone_error(t *testing.T) {
}
}
-func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.PersonalToken = "secret"
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
+ provider, err := NewDNSProviderConfig(config)
+ if err != nil {
+ return nil, err
+ }
- config := NewDefaultConfig()
- config.PersonalToken = "secret"
+ clientConfig := provider.client.GetConfig()
+ clientConfig.HTTPClient = server.Client()
+ clientConfig.Servers = idns.ServerConfigurations{{
+ URL: server.URL,
+ Description: "Production",
+ }}
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- clientConfig := provider.client.GetConfig()
- clientConfig.HTTPClient = server.Client()
- clientConfig.Servers = idns.ServerConfigurations{
- {
- URL: server.URL,
- Description: "Production",
+ return provider, nil
},
- }
-
- return provider, mux
-}
-
-func writeFixtureHandler(filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- rw.Header().Set("Content-Type", "application/json")
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
- }
+ )
}
diff --git a/providers/dns/bluecat/internal/client_test.go b/providers/dns/bluecat/internal/client_test.go
index c06ae1b8b..9d79f46b3 100644
--- a/providers/dns/bluecat/internal/client_test.go
+++ b/providers/dns/bluecat/internal/client_test.go
@@ -6,33 +6,37 @@ import (
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestClient_LookupParentZoneID(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient(server.URL, "user", "secret")
client.HTTPClient = server.Client()
- mux.HandleFunc("/Services/REST/v1/getEntityByName", func(rw http.ResponseWriter, req *http.Request) {
- query := req.URL.Query()
+ return client, nil
+}
- if query.Get("name") == "com" {
- _ = json.NewEncoder(rw).Encode(EntityResponse{
- ID: 2,
- Name: "com",
- Type: ZoneType,
- Properties: "test",
- })
- return
- }
+func TestClient_LookupParentZoneID(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /Services/REST/v1/getEntityByName",
+ http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ query := req.URL.Query()
- http.Error(rw, "{}", http.StatusOK)
- })
+ if query.Get("name") == "com" {
+ _ = json.NewEncoder(rw).Encode(EntityResponse{
+ ID: 2,
+ Name: "com",
+ Type: ZoneType,
+ Properties: "test",
+ })
+ return
+ }
+
+ _, _ = rw.Write([]byte(`{}`))
+ })).
+ Build(t)
parentID, name, err := client.LookupParentZoneID(t.Context(), 2, "foo.example.com")
require.NoError(t, err)
diff --git a/providers/dns/bluecat/internal/identity_test.go b/providers/dns/bluecat/internal/identity_test.go
index 3d9e00c0e..9ad4c18e6 100644
--- a/providers/dns/bluecat/internal/identity_test.go
+++ b/providers/dns/bluecat/internal/identity_test.go
@@ -1,11 +1,9 @@
package internal
import (
- "fmt"
- "net/http"
- "net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -13,39 +11,16 @@ import (
const fakeToken = "BAMAuthToken: dQfuRMTUxNjc3MjcyNDg1ODppcGFybXM="
func TestClient_CreateAuthenticatedContext(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(server.URL, "user", "secret")
- client.HTTPClient = server.Client()
-
- mux.HandleFunc("/Services/REST/v1/login", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- query := req.URL.Query()
- if query.Get("username") != "user" {
- http.Error(rw, fmt.Sprintf("invalid username %s", query.Get("username")), http.StatusUnauthorized)
- return
- }
-
- if query.Get("password") != "secret" {
- http.Error(rw, fmt.Sprintf("invalid password %s", query.Get("password")), http.StatusUnauthorized)
- return
- }
-
- _, _ = fmt.Fprint(rw, fakeToken)
- })
- mux.HandleFunc("/Services/REST/v1/delete", func(rw http.ResponseWriter, req *http.Request) {
- authorization := req.Header.Get(authorizationHeader)
- if authorization != fakeToken {
- http.Error(rw, fmt.Sprintf("invalid credential: %s", authorization), http.StatusUnauthorized)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /Services/REST/v1/login",
+ servermock.RawStringResponse(fakeToken),
+ servermock.CheckQueryParameter().
+ With("username", "user").
+ With("password", "secret")).
+ Route("DELETE /Services/REST/v1/delete", nil,
+ servermock.CheckHeader().
+ WithAuthorization(fakeToken)).
+ Build(t)
ctx, err := client.CreateAuthenticatedContext(t.Context())
require.NoError(t, err)
diff --git a/providers/dns/bookmyname/internal/client_test.go b/providers/dns/bookmyname/internal/client_test.go
index 26e5f7227..900d62fef 100644
--- a/providers/dns/bookmyname/internal/client_test.go
+++ b/providers/dns/bookmyname/internal/client_test.go
@@ -1,58 +1,42 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("user", "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL = server.URL
- mux.HandleFunc("GET /", func(rw http.ResponseWriter, req *http.Request) {
- username, password, ok := req.BasicAuth()
- if username != "user" || password != "secret" || !ok {
- http.Error(rw, fmt.Sprintf("auth: user %s, password %s, malformed", username, password), http.StatusOK)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(http.StatusOK)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("user", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL = server.URL
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithBasicAuth("user", "secret"))
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "add_success.txt")
+ client := mockBuilder().
+ Route("GET /",
+ servermock.ResponseFromFixture("add_success.txt"),
+ servermock.CheckQueryParameter().Strict().
+ With("do", "add").
+ With("hostname", "_acme-challenge.sub.example.com.").
+ With("type", "txt").
+ With("value", "test").
+ With("ttl", "300"),
+ ).
+ Build(t)
record := Record{
Hostname: "_acme-challenge.sub.example.com.",
@@ -66,7 +50,12 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "error.txt")
+ client := mockBuilder().
+ Route("GET /",
+ servermock.ResponseFromFixture("error.txt"),
+ servermock.CheckQueryParameter().
+ With("do", "add")).
+ Build(t)
record := Record{
Hostname: "_acme-challenge.sub.example.com.",
@@ -82,7 +71,17 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_RemoveRecord(t *testing.T) {
- client := setupTest(t, "remove_success.txt")
+ client := mockBuilder().
+ Route("GET /",
+ servermock.ResponseFromFixture("remove_success.txt"),
+ servermock.CheckQueryParameter().Strict().
+ With("do", "remove").
+ With("hostname", "_acme-challenge.sub.example.com.").
+ With("type", "txt").
+ With("value", "test").
+ With("ttl", "300"),
+ ).
+ Build(t)
record := Record{
Hostname: "_acme-challenge.sub.example.com.",
@@ -96,7 +95,12 @@ func TestClient_RemoveRecord(t *testing.T) {
}
func TestClient_RemoveRecord_error(t *testing.T) {
- client := setupTest(t, "error.txt")
+ client := mockBuilder().
+ Route("GET /",
+ servermock.ResponseFromFixture("error.txt"),
+ servermock.CheckQueryParameter().
+ With("do", "remove")).
+ Build(t)
record := Record{
Hostname: "_acme-challenge.sub.example.com.",
diff --git a/providers/dns/brandit/internal/client_test.go b/providers/dns/brandit/internal/client_test.go
index 0e79e5799..cb779ef68 100644
--- a/providers/dns/brandit/internal/client_test.go
+++ b/providers/dns/brandit/internal/client_test.go
@@ -1,49 +1,42 @@
package internal
import (
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("user", "secret")
+ if err != nil {
+ return nil, err
+ }
- server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
+ client.HTTPClient = server.Client()
+ client.baseURL = server.URL
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(http.StatusOK)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }))
- t.Cleanup(server.Close)
-
- client, err := NewClient("test_user", "apiKey")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL = server.URL
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded())
}
func TestClient_StatusDomain(t *testing.T) {
- client := setupTest(t, "status-domain.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("status-domain.json"),
+ servermock.CheckForm().Strict().
+ WithRegexp("signature", "[a-z0-9]+").
+ WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`).
+ With("command", "statusDomain").
+ With("user", "user").
+ With("domain", "example.com"),
+ ).
+ Build(t)
domain, err := client.StatusDomain(t.Context(), "example.com")
require.NoError(t, err)
@@ -79,14 +72,26 @@ func TestClient_StatusDomain(t *testing.T) {
}
func TestClient_StatusDomain_error(t *testing.T) {
- client := setupTest(t, "error.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("error.json")).
+ Build(t)
_, err := client.StatusDomain(t.Context(), "example.com")
require.ErrorIs(t, err, APIError{Code: 402, Status: "error", Message: "Invalid user."})
}
func TestClient_ListRecords(t *testing.T) {
- client := setupTest(t, "list-records.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("list-records.json"),
+ servermock.CheckForm().Strict().
+ WithRegexp("signature", "[a-z0-9]+").
+ WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`).
+ With("account", "example").
+ With("command", "listDNSRR").
+ With("user", "user").
+ With("dnszone", "example.com"),
+ ).
+ Build(t)
resp, err := client.ListRecords(t.Context(), "example", "example.com")
require.NoError(t, err)
@@ -105,14 +110,28 @@ func TestClient_ListRecords(t *testing.T) {
}
func TestClient_ListRecords_error(t *testing.T) {
- client := setupTest(t, "error.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("error.json")).
+ Build(t)
_, err := client.ListRecords(t.Context(), "example", "example.com")
require.ErrorIs(t, err, APIError{Code: 402, Status: "error", Message: "Invalid user."})
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "add-record.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("add-record.json"),
+ servermock.CheckForm().Strict().
+ WithRegexp("signature", "[a-z0-9]+").
+ WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`).
+ With("account", "test").
+ With("command", "addDNSRR").
+ With("key", "2565").
+ With("user", "user").
+ With("rrdata", "example.com 600 IN TXT txttxttxt").
+ With("dnszone", "example.com"),
+ ).
+ Build(t)
testRecord := Record{
ID: 2565,
@@ -139,7 +158,9 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "error.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("error.json")).
+ Build(t)
testRecord := Record{
ID: 2565,
@@ -154,14 +175,28 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "delete-record.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("delete-record.json"),
+ servermock.CheckForm().Strict().
+ WithRegexp("signature", "[a-z0-9]+").
+ WithRegexp("timestamp", `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`).
+ With("account", "test").
+ With("command", "deleteDNSRR").
+ With("key", "2374").
+ With("user", "user").
+ With("rrdata", "example.com 600 IN TXT txttxttxt").
+ With("dnszone", "example.com"),
+ ).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", "test", "example.com 600 IN TXT txttxttxt", "2374")
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "error.json")
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("error.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", "test", "example.com 600 IN TXT txttxttxt", "2374")
require.ErrorIs(t, err, APIError{Code: 402, Status: "error", Message: "Invalid user."})
diff --git a/providers/dns/checkdomain/internal/client_test.go b/providers/dns/checkdomain/internal/client_test.go
index 60d55ee5e..31d419a5f 100644
--- a/providers/dns/checkdomain/internal/client_test.go
+++ b/providers/dns/checkdomain/internal/client_test.go
@@ -1,70 +1,38 @@
package internal
import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "reflect"
"testing"
"github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"))
+ client.BaseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"))
- client.BaseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func checkAuthorizationHeader(req *http.Request) error {
- val := req.Header.Get("Authorization")
- if val != "Bearer secret" {
- return fmt.Errorf("invalid header value, got: %s want %s", val, "Bearer secret")
- }
- return nil
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer secret"))
}
func TestClient_GetDomainIDByName(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- err := checkAuthorizationHeader(req)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- domainList := DomainListingResponse{
- Embedded: EmbeddedDomainList{Domains: []*Domain{
- {ID: 1, Name: "test.com"},
- {ID: 2, Name: "test.org"},
- }},
- }
-
- err = json.NewEncoder(rw).Encode(domainList)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /v1/domains",
+ servermock.JSONEncode(DomainListingResponse{
+ Embedded: EmbeddedDomainList{Domains: []*Domain{
+ {ID: 1, Name: "test.com"},
+ {ID: 2, Name: "test.org"},
+ }},
+ })).
+ Build(t)
id, err := client.GetDomainIDByName(t.Context(), "test.com")
require.NoError(t, err)
@@ -73,65 +41,26 @@ func TestClient_GetDomainIDByName(t *testing.T) {
}
func TestClient_CheckNameservers(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- err := checkAuthorizationHeader(req)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- nsResp := NameserverResponse{
- Nameservers: []*Nameserver{
- {Name: ns1},
- {Name: ns2},
- // {Name: "ns.fake.de"},
- },
- }
-
- err = json.NewEncoder(rw).Encode(nsResp)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /v1/domains/1/nameservers",
+ servermock.JSONEncode(NameserverResponse{
+ Nameservers: []*Nameserver{
+ {Name: ns1},
+ {Name: ns2},
+ // {Name: "ns.fake.de"},
+ },
+ })).
+ Build(t)
err := client.CheckNameservers(t.Context(), 1)
require.NoError(t, err)
}
func TestClient_CreateRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/1/nameservers/records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- err := checkAuthorizationHeader(req)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- content, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- if string(bytes.TrimSpace(content)) != `{"name":"test.com","value":"value","ttl":300,"priority":0,"type":"TXT"}` {
- http.Error(rw, "invalid request body: "+string(content), http.StatusBadRequest)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /v1/domains/1/nameservers/records", nil,
+ servermock.CheckRequestJSONBodyFromFile("create_record-request.json")).
+ Build(t)
record := &Record{
Name: "test.com",
@@ -145,114 +74,44 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_DeleteTXTRecord(t *testing.T) {
- client, mux := setupTest(t)
-
domainName := "lego.test"
recordValue := "test"
- records := []*Record{
- {
- Name: "_acme-challenge",
- Value: recordValue,
- Type: "TXT",
- },
- {
- Name: "_acme-challenge",
- Value: recordValue,
- Type: "A",
- },
- {
- Name: "foobar",
- Value: recordValue,
- Type: "TXT",
- },
- }
-
- expectedRecords := []*Record{
- {
- Name: "_acme-challenge",
- Value: recordValue,
- Type: "A",
- },
- {
- Name: "foobar",
- Value: recordValue,
- Type: "TXT",
- },
- }
-
- mux.HandleFunc("/v1/domains/1", func(rw http.ResponseWriter, req *http.Request) {
- err := checkAuthorizationHeader(req)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- resp := DomainResponse{
- ID: 1,
- Name: domainName,
- }
-
- err = json.NewEncoder(rw).Encode(resp)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- mux.HandleFunc("/v1/domains/1/nameservers", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- nsResp := NameserverResponse{
- Nameservers: []*Nameserver{{Name: ns1}, {Name: ns2}},
- }
-
- err := json.NewEncoder(rw).Encode(nsResp)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- mux.HandleFunc("/v1/domains/1/nameservers/records", func(rw http.ResponseWriter, req *http.Request) {
- switch req.Method {
- case http.MethodGet:
- resp := RecordListingResponse{
+ client := mockBuilder().
+ Route("GET /v1/domains/",
+ servermock.JSONEncode(DomainResponse{
+ ID: 1,
+ Name: domainName,
+ })).
+ Route("GET /v1/domains/1/nameservers",
+ servermock.JSONEncode(NameserverResponse{
+ Nameservers: []*Nameserver{{Name: ns1}, {Name: ns2}},
+ })).
+ Route("GET /v1/domains/1/nameservers/records",
+ servermock.JSONEncode(RecordListingResponse{
Embedded: EmbeddedRecordList{
- Records: records,
+ Records: []*Record{
+ {
+ Name: "_acme-challenge",
+ Value: recordValue,
+ Type: "TXT",
+ },
+ {
+ Name: "_acme-challenge",
+ Value: recordValue,
+ Type: "A",
+ },
+ {
+ Name: "foobar",
+ Value: recordValue,
+ Type: "TXT",
+ },
+ },
},
- }
-
- err := json.NewEncoder(rw).Encode(resp)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- case http.MethodPut:
- var records []*Record
- err := json.NewDecoder(req.Body).Decode(&records)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- if len(records) == 0 {
- http.Error(rw, "empty request body", http.StatusBadRequest)
- return
- }
-
- if !reflect.DeepEqual(expectedRecords, records) {
- http.Error(rw, fmt.Sprintf("invalid records: %v", records), http.StatusBadRequest)
- return
- }
- default:
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- }
- })
+ })).
+ Route("PUT /v1/domains/1/nameservers/records", nil,
+ servermock.CheckRequestJSONBodyFromFile("delete_txt_record-request.json")).
+ Build(t)
info := dns01.GetChallengeInfo(domainName, "abc")
err := client.DeleteTXTRecord(t.Context(), 1, info.EffectiveFQDN, recordValue)
diff --git a/providers/dns/checkdomain/internal/fixtures/create_record-request.json b/providers/dns/checkdomain/internal/fixtures/create_record-request.json
new file mode 100644
index 000000000..af1d50625
--- /dev/null
+++ b/providers/dns/checkdomain/internal/fixtures/create_record-request.json
@@ -0,0 +1,7 @@
+{
+ "name": "test.com",
+ "value": "value",
+ "ttl": 300,
+ "priority": 0,
+ "type": "TXT"
+}
diff --git a/providers/dns/checkdomain/internal/fixtures/delete_txt_record-request.json b/providers/dns/checkdomain/internal/fixtures/delete_txt_record-request.json
new file mode 100644
index 000000000..67cb2570c
--- /dev/null
+++ b/providers/dns/checkdomain/internal/fixtures/delete_txt_record-request.json
@@ -0,0 +1,16 @@
+[
+ {
+ "name": "_acme-challenge",
+ "value": "test",
+ "ttl": 0,
+ "priority": 0,
+ "type": "A"
+ },
+ {
+ "name": "foobar",
+ "value": "test",
+ "ttl": 0,
+ "priority": 0,
+ "type": "TXT"
+ }
+]
diff --git a/providers/dns/clouddns/internal/client_test.go b/providers/dns/clouddns/internal/client_test.go
index 2dee0bd0f..a8092933c 100644
--- a/providers/dns/clouddns/internal/client_test.go
+++ b/providers/dns/clouddns/internal/client_test.go
@@ -1,127 +1,63 @@
package internal
import (
- "encoding/json"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("clientID", "email@example.com", "secret", 300)
+ client.HTTPClient = server.Client()
+ client.apiBaseURL, _ = url.Parse(server.URL + "/api")
+ client.loginURL, _ = url.Parse(server.URL + "/login")
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("clientID", "email@example.com", "secret", 300)
- client.HTTPClient = server.Client()
- client.apiBaseURL, _ = url.Parse(server.URL + "/api")
- client.loginURL, _ = url.Parse(server.URL + "/login")
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestClient_AddRecord(t *testing.T) {
- client, mux := setupTest(t)
+ client := mockBuilder().
+ Route("POST /api/domain/search",
+ servermock.ResponseFromFixture("domain_search.json"),
+ servermock.CheckRequestJSONBodyFromFile("domain_search-request.json")).
+ Route("POST /api/record-txt", nil,
+ servermock.CheckRequestJSONBodyFromFile("record_txt-request.json")).
+ Route("PUT /api/domain/A/publish", nil,
+ servermock.CheckRequestJSONBodyFromFile("publish-request.json")).
+ Route("POST /login",
+ servermock.ResponseFromFixture("login.json"),
+ servermock.CheckRequestJSONBodyFromFile("login-request.json")).
+ Build(t)
- mux.HandleFunc("/api/domain/search", func(rw http.ResponseWriter, req *http.Request) {
- response := SearchResponse{
- Items: []Domain{
- {
- ID: "A",
- DomainName: "example.com",
- },
- },
- }
+ ctx, err := client.CreateAuthenticatedContext(t.Context())
+ require.NoError(t, err)
- err := json.NewEncoder(rw).Encode(response)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
- mux.HandleFunc("/api/record-txt", func(rw http.ResponseWriter, req *http.Request) {})
- mux.HandleFunc("/api/domain/A/publish", func(rw http.ResponseWriter, req *http.Request) {})
- mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) {
- response := AuthResponse{
- Auth: Auth{
- AccessToken: "at",
- RefreshToken: "",
- },
- }
-
- err := json.NewEncoder(rw).Encode(response)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- err := client.AddRecord(t.Context(), "example.com", "_acme-challenge.example.com", "txt")
+ err = client.AddRecord(ctx, "example.com", "_acme-challenge.example.com", "txt")
require.NoError(t, err)
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/api/domain/search", func(rw http.ResponseWriter, req *http.Request) {
- response := SearchResponse{
- Items: []Domain{
- {
- ID: "A",
- DomainName: "example.com",
- },
- },
- }
-
- err := json.NewEncoder(rw).Encode(response)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
- mux.HandleFunc("/api/domain/A", func(rw http.ResponseWriter, req *http.Request) {
- response := DomainInfo{
- ID: "Z",
- DomainName: "example.com",
- LastDomainRecordList: []Record{
- {
- ID: "R01",
- DomainID: "A",
- Name: "_acme-challenge.example.com",
- Value: "txt",
- Type: "TXT",
- },
- },
- SoaTTL: 300,
- }
-
- err := json.NewEncoder(rw).Encode(response)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
- mux.HandleFunc("/api/record/R01", func(rw http.ResponseWriter, req *http.Request) {})
- mux.HandleFunc("/api/domain/A/publish", func(rw http.ResponseWriter, req *http.Request) {})
- mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) {
- response := AuthResponse{
- Auth: Auth{
- AccessToken: "at",
- RefreshToken: "",
- },
- }
-
- err := json.NewEncoder(rw).Encode(response)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /api/domain/search",
+ servermock.ResponseFromFixture("domain_search.json"),
+ servermock.CheckRequestJSONBodyFromFile("domain_search-request.json")).
+ Route("GET /api/domain/A",
+ servermock.ResponseFromFixture("domain-request.json")).
+ Route("DELETE /api/record/R01", nil).
+ Route("PUT /api/domain/A/publish", nil,
+ servermock.CheckRequestJSONBodyFromFile("publish-request.json")).
+ Route("POST /login",
+ servermock.ResponseFromFixture("login.json"),
+ servermock.CheckRequestJSONBodyFromFile("login-request.json")).
+ Build(t)
ctx, err := client.CreateAuthenticatedContext(t.Context())
require.NoError(t, err)
diff --git a/providers/dns/clouddns/internal/fixtures/domain-request.json b/providers/dns/clouddns/internal/fixtures/domain-request.json
new file mode 100644
index 000000000..00f60b9bd
--- /dev/null
+++ b/providers/dns/clouddns/internal/fixtures/domain-request.json
@@ -0,0 +1,14 @@
+{
+ "id": "Z",
+ "domainName": "example.com",
+ "lastDomainRecordList": [
+ {
+ "id": "R01",
+ "domainId": "A",
+ "name": "_acme-challenge.example.com",
+ "value": "txt",
+ "type": "TXT"
+ }
+ ],
+ "soaTtl": 300
+}
diff --git a/providers/dns/clouddns/internal/fixtures/domain_search-request.json b/providers/dns/clouddns/internal/fixtures/domain_search-request.json
new file mode 100644
index 000000000..89043dc3a
--- /dev/null
+++ b/providers/dns/clouddns/internal/fixtures/domain_search-request.json
@@ -0,0 +1,14 @@
+{
+ "search": [
+ {
+ "name": "clientId",
+ "operator": "eq",
+ "value": "clientID"
+ },
+ {
+ "name": "domainName",
+ "operator": "eq",
+ "value": "example.com"
+ }
+ ]
+}
diff --git a/providers/dns/clouddns/internal/fixtures/domain_search.json b/providers/dns/clouddns/internal/fixtures/domain_search.json
new file mode 100644
index 000000000..4ee454732
--- /dev/null
+++ b/providers/dns/clouddns/internal/fixtures/domain_search.json
@@ -0,0 +1,8 @@
+{
+ "items": [
+ {
+ "id": "A",
+ "domainName": "example.com"
+ }
+ ]
+}
diff --git a/providers/dns/clouddns/internal/fixtures/login-request.json b/providers/dns/clouddns/internal/fixtures/login-request.json
new file mode 100644
index 000000000..132577e6b
--- /dev/null
+++ b/providers/dns/clouddns/internal/fixtures/login-request.json
@@ -0,0 +1,4 @@
+{
+ "email": "email@example.com",
+ "password": "secret"
+}
diff --git a/providers/dns/clouddns/internal/fixtures/login.json b/providers/dns/clouddns/internal/fixtures/login.json
new file mode 100644
index 000000000..e72ffb19b
--- /dev/null
+++ b/providers/dns/clouddns/internal/fixtures/login.json
@@ -0,0 +1,5 @@
+{
+ "auth": {
+ "accessToken": "at"
+ }
+}
diff --git a/providers/dns/clouddns/internal/fixtures/publish-request.json b/providers/dns/clouddns/internal/fixtures/publish-request.json
new file mode 100644
index 000000000..383e26958
--- /dev/null
+++ b/providers/dns/clouddns/internal/fixtures/publish-request.json
@@ -0,0 +1,3 @@
+{
+ "soaTtl": 300
+}
diff --git a/providers/dns/clouddns/internal/fixtures/record_txt-request.json b/providers/dns/clouddns/internal/fixtures/record_txt-request.json
new file mode 100644
index 000000000..cbc2a32a0
--- /dev/null
+++ b/providers/dns/clouddns/internal/fixtures/record_txt-request.json
@@ -0,0 +1,6 @@
+{
+ "domainId": "A",
+ "name": "_acme-challenge.example.com",
+ "value": "txt",
+ "type": "TXT"
+}
diff --git a/providers/dns/clouddns/internal/identity_test.go b/providers/dns/clouddns/internal/identity_test.go
index a3f3f55ea..df5e20eb8 100644
--- a/providers/dns/clouddns/internal/identity_test.go
+++ b/providers/dns/clouddns/internal/identity_test.go
@@ -1,38 +1,20 @@
package internal
import (
- "encoding/json"
- "net/http"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClient_CreateAuthenticatedContext(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) {
- response := AuthResponse{
- Auth: Auth{
- AccessToken: "at",
- RefreshToken: "",
- },
- }
-
- err := json.NewEncoder(rw).Encode(response)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
- mux.HandleFunc("/api/record/xxx", func(rw http.ResponseWriter, req *http.Request) {
- authorization := req.Header.Get(authorizationHeader)
- if authorization != "Bearer at" {
- http.Error(rw, "invalid credential: "+authorization, http.StatusUnauthorized)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /login",
+ servermock.ResponseFromFixture("login.json"),
+ servermock.CheckRequestJSONBodyFromFile("login-request.json")).
+ Route("DELETE /api/record/xxx", nil).
+ Build(t)
ctx, err := client.CreateAuthenticatedContext(t.Context())
require.NoError(t, err)
diff --git a/providers/dns/cloudns/internal/client_test.go b/providers/dns/cloudns/internal/client_test.go
index e5d10b089..dbfa32aee 100644
--- a/providers/dns/cloudns/internal/client_test.go
+++ b/providers/dns/cloudns/internal/client_test.go
@@ -1,43 +1,25 @@
package internal
import (
- "fmt"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, subAuthID string, handler http.HandlerFunc) *Client {
- t.Helper()
-
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- client, err := NewClient("myAuthID", subAuthID, "myAuthPassword")
- require.NoError(t, err)
-
- client.BaseURL, _ = url.Parse(server.URL)
- client.HTTPClient = server.Client()
-
- return client
-}
-
-func handlerMock(method string, jsonData []byte) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, "Incorrect method used", http.StatusBadRequest)
- return
- }
-
- _, err := rw.Write(jsonData)
+func setupClient(subAuthID string) func(server *httptest.Server) (*Client, error) {
+ return func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("myAuthID", subAuthID, "myAuthPassword")
if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
+ return nil, err
}
+
+ client.BaseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
+ return client, nil
}
}
@@ -131,7 +113,15 @@ func TestClient_GetZone(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse)))
+ client := servermock.NewBuilder[*Client](setupClient("")).
+ Route("GET /get-zone-info.json",
+ servermock.RawStringResponse(test.apiResponse),
+ servermock.CheckQueryParameter().Strict().
+ With("auth-id", "myAuthID").
+ With("auth-password", "myAuthPassword").
+ With("domain-name", "foo.com"),
+ ).
+ Build(t)
zone, err := client.GetZone(t.Context(), test.authFQDN)
@@ -238,7 +228,17 @@ func TestClient_FindTxtRecord(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse)))
+ client := servermock.NewBuilder[*Client](setupClient("")).
+ Route("GET /records.json",
+ servermock.RawStringResponse(test.apiResponse),
+ servermock.CheckQueryParameter().Strict().
+ With("auth-id", "myAuthID").
+ With("auth-password", "myAuthPassword").
+ With("type", "TXT").
+ With("host", "_acme-challenge").
+ With("domain-name", test.zoneName),
+ ).
+ Build(t)
txtRecord, err := client.FindTxtRecord(t.Context(), test.zoneName, test.authFQDN)
@@ -347,7 +347,17 @@ func TestClient_ListTxtRecord(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := setupTest(t, "", handlerMock(http.MethodGet, []byte(test.apiResponse)))
+ client := servermock.NewBuilder[*Client](setupClient("")).
+ Route("GET /records.json",
+ servermock.RawStringResponse(test.apiResponse),
+ servermock.CheckQueryParameter().Strict().
+ With("auth-id", "myAuthID").
+ With("auth-password", "myAuthPassword").
+ With("type", "TXT").
+ With("host", "_acme-challenge").
+ With("domain-name", test.zoneName),
+ ).
+ Build(t)
txtRecords, err := client.ListTxtRecords(t.Context(), test.zoneName, test.authFQDN)
@@ -363,7 +373,7 @@ func TestClient_ListTxtRecord(t *testing.T) {
func TestClient_AddTxtRecord(t *testing.T) {
type expected struct {
- query string
+ query url.Values
errorMsg string
}
@@ -387,7 +397,15 @@ func TestClient_AddTxtRecord(t *testing.T) {
ttl: 60,
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
expected: expected{
- query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge.foo&record=txtTXTtxtTXTtxtTXTtxtTXT&record-type=TXT&ttl=60`,
+ query: url.Values{
+ "auth-id": {"myAuthID"},
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"example.com"},
+ "host": {"_acme-challenge.foo"},
+ "record": {"txtTXTtxtTXTtxtTXTtxtTXT"},
+ "record-type": {"TXT"},
+ "ttl": {"60"},
+ },
},
},
{
@@ -399,7 +417,15 @@ func TestClient_AddTxtRecord(t *testing.T) {
ttl: 60,
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
expected: expected{
- query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=60`,
+ query: url.Values{
+ "auth-id": {"myAuthID"},
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"example.com"},
+ "host": {"_acme-challenge"},
+ "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"},
+ "record-type": {"TXT"},
+ "ttl": {"60"},
+ },
},
},
{
@@ -411,7 +437,15 @@ func TestClient_AddTxtRecord(t *testing.T) {
ttl: 60,
apiResponse: `{"status":"Success","statusDescription":"The record was added successfully."}`,
expected: expected{
- query: `auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&sub-auth-id=mySubAuthID&ttl=60`,
+ query: url.Values{
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"example.com"},
+ "host": {"_acme-challenge"},
+ "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"},
+ "record-type": {"TXT"},
+ "sub-auth-id": {"mySubAuthID"},
+ "ttl": {"60"},
+ },
},
},
{
@@ -423,7 +457,15 @@ func TestClient_AddTxtRecord(t *testing.T) {
ttl: 120,
apiResponse: `{"status":"Failed","statusDescription":"Invalid TTL. Choose from the list of the values we support."}`,
expected: expected{
- query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
+ query: url.Values{
+ "auth-id": {"myAuthID"},
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"example.com"},
+ "host": {"_acme-challenge"},
+ "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"},
+ "record-type": {"TXT"},
+ "ttl": {"300"},
+ },
errorMsg: "failed to add TXT record: Failed Invalid TTL. Choose from the list of the values we support.",
},
},
@@ -436,7 +478,15 @@ func TestClient_AddTxtRecord(t *testing.T) {
ttl: 120,
apiResponse: `[{}]`,
expected: expected{
- query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=example.com&host=_acme-challenge&record=TXTtxtTXTtxtTXTtxtTXTtxt&record-type=TXT&ttl=300`,
+ query: url.Values{
+ "auth-id": {"myAuthID"},
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"example.com"},
+ "host": {"_acme-challenge"},
+ "record": {"TXTtxtTXTtxtTXTtxtTXTtxt"},
+ "record-type": {"TXT"},
+ "ttl": {"300"},
+ },
errorMsg: "unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.apiResponse",
},
},
@@ -444,15 +494,13 @@ func TestClient_AddTxtRecord(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := setupTest(t, test.subAuthID, func(rw http.ResponseWriter, req *http.Request) {
- if test.expected.query != req.URL.RawQuery {
- msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery)
- http.Error(rw, msg, http.StatusBadRequest)
- return
- }
-
- handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req)
- })
+ client := servermock.NewBuilder[*Client](setupClient(test.subAuthID)).
+ Route("POST /add-record.json",
+ servermock.RawStringResponse(test.apiResponse),
+ servermock.CheckQueryParameter().Strict().
+ WithValues(test.expected.query),
+ ).
+ Build(t)
err := client.AddTxtRecord(t.Context(), test.zoneName, test.authFQDN, test.value, test.ttl)
@@ -467,7 +515,7 @@ func TestClient_AddTxtRecord(t *testing.T) {
func TestClient_RemoveTxtRecord(t *testing.T) {
type expected struct {
- query string
+ query url.Values
errorMsg string
}
@@ -484,7 +532,12 @@ func TestClient_RemoveTxtRecord(t *testing.T) {
zoneName: "foo.com",
apiResponse: `{ "status": "Success", "statusDescription": "The record was deleted successfully." }`,
expected: expected{
- query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769228`,
+ query: url.Values{
+ "auth-id": {"myAuthID"},
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"foo.com"},
+ "record-id": {"5769228"},
+ },
},
},
{
@@ -493,7 +546,12 @@ func TestClient_RemoveTxtRecord(t *testing.T) {
zoneName: "foo.com",
apiResponse: `{ "status": "Failed", "statusDescription": "Invalid record-id param." }`,
expected: expected{
- query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo.com&record-id=5769000`,
+ query: url.Values{
+ "auth-id": {"myAuthID"},
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"foo.com"},
+ "record-id": {"5769000"},
+ },
errorMsg: "failed to remove TXT record: Failed Invalid record-id param.",
},
},
@@ -503,7 +561,12 @@ func TestClient_RemoveTxtRecord(t *testing.T) {
zoneName: "foo-plus.com",
apiResponse: `[{}]`,
expected: expected{
- query: `auth-id=myAuthID&auth-password=myAuthPassword&domain-name=foo-plus.com&record-id=44`,
+ query: url.Values{
+ "auth-id": {"myAuthID"},
+ "auth-password": {"myAuthPassword"},
+ "domain-name": {"foo-plus.com"},
+ "record-id": {"44"},
+ },
errorMsg: "unable to unmarshal response: [status code: 200] body: [{}] error: json: cannot unmarshal array into Go value of type internal.apiResponse",
},
},
@@ -511,23 +574,15 @@ func TestClient_RemoveTxtRecord(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
- if test.expected.query != req.URL.RawQuery {
- msg := fmt.Sprintf("got: %s, want: %s", test.expected.query, req.URL.RawQuery)
- http.Error(rw, msg, http.StatusBadRequest)
- return
- }
+ client := servermock.NewBuilder[*Client](setupClient("")).
+ Route("POST /delete-record.json",
+ servermock.RawStringResponse(test.apiResponse),
+ servermock.CheckQueryParameter().Strict().
+ WithValues(test.expected.query),
+ ).
+ Build(t)
- handlerMock(http.MethodPost, []byte(test.apiResponse))(rw, req)
- }))
- t.Cleanup(server.Close)
-
- client, err := NewClient("myAuthID", "", "myAuthPassword")
- require.NoError(t, err)
-
- client.BaseURL, _ = url.Parse(server.URL)
-
- err = client.RemoveTxtRecord(t.Context(), test.id, test.zoneName)
+ err := client.RemoveTxtRecord(t.Context(), test.id, test.zoneName)
if test.expected.errorMsg != "" {
require.EqualError(t, err, test.expected.errorMsg)
@@ -589,13 +644,15 @@ func TestClient_GetUpdateStatus(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- server := httptest.NewServer(handlerMock(http.MethodGet, []byte(test.apiResponse)))
- t.Cleanup(server.Close)
-
- client, err := NewClient("myAuthID", "", "myAuthPassword")
- require.NoError(t, err)
-
- client.BaseURL, _ = url.Parse(server.URL)
+ client := servermock.NewBuilder[*Client](setupClient("")).
+ Route("GET /update-status.json",
+ servermock.RawStringResponse(test.apiResponse),
+ servermock.CheckQueryParameter().Strict().
+ With("auth-id", "myAuthID").
+ With("auth-password", "myAuthPassword").
+ With("domain-name", test.zoneName),
+ ).
+ Build(t)
syncProgress, err := client.GetUpdateStatus(t.Context(), test.zoneName)
diff --git a/providers/dns/cloudru/internal/client_test.go b/providers/dns/cloudru/internal/client_test.go
index 21e227f76..3b087d617 100644
--- a/providers/dns/cloudru/internal/client_test.go
+++ b/providers/dns/cloudru/internal/client_test.go
@@ -1,62 +1,40 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
"time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.APIEndpoint, _ = url.Parse(server.URL)
+ client.token = &Token{
+ AccessToken: "secret",
+ ExpiresIn: 60,
+ TokenType: "Bearer",
+ Deadline: time.Now().Add(1 * time.Minute),
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, handler)
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.APIEndpoint, _ = url.Parse(server.URL)
- client.token = &Token{
- AccessToken: "secret",
- ExpiresIn: 60,
- TokenType: "Bearer",
- Deadline: time.Now().Add(1 * time.Minute),
- }
-
- return client
-}
-
-func writeFixtureHandler(method, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
- }
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer xxx"))
}
func TestClient_GetZones(t *testing.T) {
- client := setupTest(t, "/zones", writeFixtureHandler(http.MethodGet, "zones.json"))
+ client := mockBuilder().
+ Route("GET /zones",
+ servermock.ResponseFromFixture("zones.json")).
+ Build(t)
ctx := mockContext(t)
@@ -78,7 +56,10 @@ func TestClient_GetZones(t *testing.T) {
}
func TestClient_GetRecords(t *testing.T) {
- client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodGet, "records.json"))
+ client := mockBuilder().
+ Route("GET /zones/zzz/records",
+ servermock.ResponseFromFixture("records.json")).
+ Build(t)
ctx := mockContext(t)
@@ -122,7 +103,11 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_CreateRecord(t *testing.T) {
- client := setupTest(t, "/zones/zzz/records", writeFixtureHandler(http.MethodPost, "record.json"))
+ client := mockBuilder().
+ Route("POST /zones/zzz/records",
+ servermock.ResponseFromFixture("record.json"),
+ servermock.CheckRequestJSONBody(`{"name":"www.example.com.","type":"TXT","values":["text"],"ttl":"3600"}`)).
+ Build(t)
ctx := mockContext(t)
@@ -150,7 +135,10 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/zones/zzz/records/example.com/TXT", writeFixtureHandler(http.MethodDelete, "record.json"))
+ client := mockBuilder().
+ Route("DELETE /zones/zzz/records/example.com/TXT",
+ servermock.ResponseFromFixture("record.json")).
+ Build(t)
ctx := mockContext(t)
diff --git a/providers/dns/cloudru/internal/identity_test.go b/providers/dns/cloudru/internal/identity_test.go
index 68dbd90cd..c1097c015 100644
--- a/providers/dns/cloudru/internal/identity_test.go
+++ b/providers/dns/cloudru/internal/identity_test.go
@@ -2,13 +2,11 @@ package internal
import (
"context"
- "encoding/json"
- "fmt"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -19,47 +17,33 @@ func mockContext(t *testing.T) context.Context {
return context.WithValue(t.Context(), tokenKey, &Token{AccessToken: "xxx"})
}
-func tokenHandler(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, http.MethodPost), http.StatusMethodNotAllowed)
- return
- }
-
- err := req.ParseForm()
- if err != nil {
- http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
- return
- }
-
- grantType := req.Form.Get("grant_type")
- clientID := req.Form.Get("client_id")
- clientSecret := req.Form.Get("client_secret")
-
- if clientID != "user" || clientSecret != "secret" || grantType != "access_key" {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- _ = json.NewEncoder(rw).Encode(Token{
- AccessToken: "xxx",
- TokenID: "yyy",
- ExpiresIn: 666,
- TokenType: "Bearer",
- Scope: "openid profile email roles",
- })
-}
-
-func TestClient_obtainToken(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", tokenHandler)
-
+func setupIdentityClient(server *httptest.Server) (*Client, error) {
client := NewClient("user", "secret")
client.HTTPClient = server.Client()
client.AuthEndpoint, _ = url.Parse(server.URL)
+ return client, nil
+}
+
+func TestClient_obtainToken(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupIdentityClient,
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ ).
+ Route("POST /", servermock.JSONEncode(Token{
+ AccessToken: "xxx",
+ TokenID: "yyy",
+ ExpiresIn: 666,
+ TokenType: "Bearer",
+ Scope: "openid profile email roles",
+ }),
+ servermock.CheckForm().Strict().
+ With("client_id", "user").
+ With("client_secret", "secret").
+ With("grant_type", "access_key"),
+ ).
+ Build(t)
+
assert.Nil(t, client.token)
tok, err := client.obtainToken(t.Context())
@@ -71,15 +55,23 @@ func TestClient_obtainToken(t *testing.T) {
}
func TestClient_CreateAuthenticatedContext(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", tokenHandler)
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.AuthEndpoint, _ = url.Parse(server.URL)
+ client := servermock.NewBuilder[*Client](setupIdentityClient,
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ ).
+ Route("POST /", servermock.JSONEncode(Token{
+ AccessToken: "xxx",
+ TokenID: "yyy",
+ ExpiresIn: 666,
+ TokenType: "Bearer",
+ Scope: "openid profile email roles",
+ }),
+ servermock.CheckForm().Strict().
+ With("client_id", "user").
+ With("client_secret", "secret").
+ With("grant_type", "access_key"),
+ ).
+ Build(t)
assert.Nil(t, client.token)
diff --git a/providers/dns/conoha/internal/client_test.go b/providers/dns/conoha/internal/client_test.go
index 0cabb30dd..0b9242c08 100644
--- a/providers/dns/conoha/internal/client_test.go
+++ b/providers/dns/conoha/internal/client_test.go
@@ -11,60 +11,26 @@ import (
"path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("tyo1", "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- client, err := NewClient("tyo1", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func writeFixtureHandler(method, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, filename)
- }
-}
-
-func writeBodyHandler(method, content string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
- _, err := fmt.Fprint(rw, content)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
-
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With("X-Auth-Token", "secret"))
}
func TestClient_GetDomainID(t *testing.T) {
@@ -76,34 +42,34 @@ func TestClient_GetDomainID(t *testing.T) {
testCases := []struct {
desc string
domainName string
- handler http.HandlerFunc
+ response string
expected expected
}{
{
desc: "success",
domainName: "domain1.com.",
- handler: writeFixtureHandler(http.MethodGet, "domains_GET.json"),
+ response: "domains_GET.json",
expected: expected{domainID: "09494b72-b65b-4297-9efb-187f65a0553e"},
},
{
desc: "non existing domain",
domainName: "domain1.com.",
- handler: writeBodyHandler(http.MethodGet, "{}"),
+ response: "empty.json",
expected: expected{error: true},
},
{
desc: "marshaling error",
domainName: "domain1.com.",
- handler: writeBodyHandler(http.MethodGet, "[]"),
+ response: "empty.json",
expected: expected{error: true},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v1/domains", test.handler)
+ client := mockBuilder().
+ Route("GET /v1/domains", servermock.ResponseFromFixture(test.response)).
+ Build(t)
domainID, err := client.GetDomainID(t.Context(), test.domainName)
@@ -126,11 +92,6 @@ func TestClient_CreateRecord(t *testing.T) {
{
desc: "success",
handler: func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
raw, err := io.ReadAll(req.Body)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
@@ -143,18 +104,20 @@ func TestClient_CreateRecord(t *testing.T) {
return
}
- writeFixture(rw, "domains-records_POST.json")
+ file, err := os.Open(filepath.Join("fixtures", "domains-records_POST.json"))
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer func() { _ = file.Close() }()
+
+ _, _ = io.Copy(rw, file)
},
assert: require.NoError,
},
{
desc: "bad request",
handler: func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
http.Error(rw, "OOPS", http.StatusBadRequest)
},
assert: require.Error,
@@ -163,9 +126,9 @@ func TestClient_CreateRecord(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v1/domains/lego/records", test.handler)
+ client := mockBuilder().
+ Route("POST /v1/domains/lego/records", test.handler).
+ Build(t)
domainID := "lego"
@@ -183,10 +146,10 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_GetRecordID(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/89acac79-38e7-497d-807c-a011e1310438/records",
- writeFixtureHandler(http.MethodGet, "domains-records_GET.json"))
+ client := mockBuilder().
+ Route("GET /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records",
+ servermock.ResponseFromFixture("domains-records_GET.json")).
+ Build(t)
recordID, err := client.GetRecordID(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "www.example.com.", "A", "15.185.172.153")
require.NoError(t, err)
@@ -195,16 +158,10 @@ func TestClient_GetRecordID(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- rw.WriteHeader(http.StatusOK)
- })
+ client := mockBuilder().
+ Route("DELETE /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad",
+ servermock.ResponseFromFixture("domains-records_GET.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "2e32e609-3a4f-45ba-bdef-e50eacd345ad")
require.NoError(t, err)
diff --git a/providers/dns/conoha/internal/fixtures/empty.json b/providers/dns/conoha/internal/fixtures/empty.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/providers/dns/conoha/internal/fixtures/empty.json
@@ -0,0 +1 @@
+{}
diff --git a/providers/dns/conoha/internal/identity_test.go b/providers/dns/conoha/internal/identity_test.go
index 77db51f09..0bd4c936a 100644
--- a/providers/dns/conoha/internal/identity_test.go
+++ b/providers/dns/conoha/internal/identity_test.go
@@ -1,27 +1,33 @@
package internal
import (
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestNewClient(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupIdentifier(server *httptest.Server) (*Identifier, error) {
identifier, err := NewIdentifier("tyo1")
- require.NoError(t, err)
+ if err != nil {
+ return nil, err
+ }
identifier.HTTPClient = server.Client()
identifier.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc("/v2.0/tokens", writeFixtureHandler(http.MethodPost, "tokens_POST.json"))
+ return identifier, nil
+}
+
+func TestNewClient(t *testing.T) {
+ identifier := servermock.NewBuilder[*Identifier](setupIdentifier,
+ servermock.CheckHeader().WithJSONHeaders(),
+ ).
+ Route("POST /v2.0/tokens", servermock.ResponseFromFixture("tokens_POST.json")).
+ Build(t)
auth := Auth{
TenantID: "487727e3921d44e3bfe7ebb337bf085e",
diff --git a/providers/dns/conohav3/internal/client_test.go b/providers/dns/conohav3/internal/client_test.go
index 9600b2f06..babdadf7e 100644
--- a/providers/dns/conohav3/internal/client_test.go
+++ b/providers/dns/conohav3/internal/client_test.go
@@ -11,60 +11,27 @@ import (
"path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("c3j1", "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- client, err := NewClient("c3j1", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func writeFixtureHandler(method, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, filename)
- }
-}
-
-func writeBodyHandler(method, content string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
- _, err := fmt.Fprint(rw, content)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
-
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithJSONHeaders().
+ With("X-Auth-Token", "secret"))
}
func TestClient_GetDomainID(t *testing.T) {
@@ -76,34 +43,34 @@ func TestClient_GetDomainID(t *testing.T) {
testCases := []struct {
desc string
domainName string
- handler http.HandlerFunc
+ response string
expected expected
}{
{
desc: "success",
domainName: "domain1.com.",
- handler: writeFixtureHandler(http.MethodGet, "domains_GET.json"),
+ response: "domains_GET.json",
expected: expected{domainID: "09494b72-b65b-4297-9efb-187f65a0553e"},
},
{
desc: "non existing domain",
domainName: "domain1.com.",
- handler: writeBodyHandler(http.MethodGet, "{}"),
+ response: "empty.json",
expected: expected{error: true},
},
{
desc: "marshaling error",
domainName: "domain1.com.",
- handler: writeBodyHandler(http.MethodGet, "[]"),
+ response: "empty.json",
expected: expected{error: true},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v1/domains", test.handler)
+ client := mockBuilder().
+ Route("GET /v1/domains", servermock.ResponseFromFixture(test.response)).
+ Build(t)
domainID, err := client.GetDomainID(t.Context(), test.domainName)
@@ -126,11 +93,6 @@ func TestClient_CreateRecord(t *testing.T) {
{
desc: "success",
handler: func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
raw, err := io.ReadAll(req.Body)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
@@ -143,18 +105,20 @@ func TestClient_CreateRecord(t *testing.T) {
return
}
- writeFixture(rw, "domains-records_POST.json")
+ file, err := os.Open(filepath.Join("fixtures", "domains-records_POST.json"))
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer func() { _ = file.Close() }()
+
+ _, _ = io.Copy(rw, file)
},
assert: require.NoError,
},
{
desc: "bad request",
handler: func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
http.Error(rw, "OOPS", http.StatusBadRequest)
},
assert: require.Error,
@@ -163,9 +127,9 @@ func TestClient_CreateRecord(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v1/domains/lego/records", test.handler)
+ client := mockBuilder().
+ Route("POST /v1/domains/lego/records", test.handler).
+ Build(t)
domainID := "lego"
@@ -183,10 +147,10 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_GetRecordID(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/89acac79-38e7-497d-807c-a011e1310438/records",
- writeFixtureHandler(http.MethodGet, "domains-records_GET.json"))
+ client := mockBuilder().
+ Route("GET /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records",
+ servermock.ResponseFromFixture("domains-records_GET.json")).
+ Build(t)
recordID, err := client.GetRecordID(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "www.example.com.", "A", "15.185.172.153")
require.NoError(t, err)
@@ -195,16 +159,10 @@ func TestClient_GetRecordID(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- rw.WriteHeader(http.StatusOK)
- })
+ client := mockBuilder().
+ Route("DELETE /v1/domains/89acac79-38e7-497d-807c-a011e1310438/records/2e32e609-3a4f-45ba-bdef-e50eacd345ad",
+ servermock.ResponseFromFixture("domains-records_GET.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "89acac79-38e7-497d-807c-a011e1310438", "2e32e609-3a4f-45ba-bdef-e50eacd345ad")
require.NoError(t, err)
diff --git a/providers/dns/conohav3/internal/fixtures/empty.json b/providers/dns/conohav3/internal/fixtures/empty.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/providers/dns/conohav3/internal/fixtures/empty.json
@@ -0,0 +1 @@
+{}
diff --git a/providers/dns/conohav3/internal/identity_test.go b/providers/dns/conohav3/internal/identity_test.go
index d5222c05d..d479a18d9 100644
--- a/providers/dns/conohav3/internal/identity_test.go
+++ b/providers/dns/conohav3/internal/identity_test.go
@@ -6,26 +6,32 @@ import (
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestGetToken_HeaderToken(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupIdentifier(server *httptest.Server) (*Identifier, error) {
identifier, err := NewIdentifier("c3j1")
- require.NoError(t, err)
+ if err != nil {
+ return nil, err
+ }
identifier.HTTPClient = server.Client()
identifier.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("x-subject-token", "sample-header-token-123")
- w.WriteHeader(http.StatusCreated)
- _, _ = w.Write([]byte(`{}`))
- })
+ return identifier, nil
+}
+
+func TestGetToken_HeaderToken(t *testing.T) {
+ identifier := servermock.NewBuilder[*Identifier](setupIdentifier,
+ servermock.CheckHeader().WithJSONHeaders(),
+ ).
+ Route("POST /v3/auth/tokens",
+ servermock.ResponseFromFixture("empty.json").
+ WithStatusCode(http.StatusCreated).
+ WithHeader("x-subject-token", "sample-header-token-123")).
+ Build(t)
auth := Auth{
Identity: Identity{
diff --git a/providers/dns/constellix/internal/domains_test.go b/providers/dns/constellix/internal/domains_test.go
index f6ade9d31..2d92fb8f3 100644
--- a/providers/dns/constellix/internal/domains_test.go
+++ b/providers/dns/constellix/internal/domains_test.go
@@ -1,51 +1,30 @@
package internal
import (
- "io"
- "net/http"
"net/http/httptest"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(server.Client())
+ client.BaseURL = server.URL
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(server.Client())
- client.BaseURL = server.URL
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestDomainService_GetAll(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/domains-GetAll.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /v1/domains", servermock.ResponseFromFixture("domains-GetAll.json")).
+ Build(t)
data, err := client.Domains.GetAll(t.Context(), nil)
require.NoError(t, err)
@@ -61,27 +40,12 @@ func TestDomainService_GetAll(t *testing.T) {
}
func TestDomainService_Search(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/search", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/domains-Search.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /v1/domains/search",
+ servermock.ResponseFromFixture("domains-Search.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("exact", "lego.wtf")).
+ Build(t)
data, err := client.Domains.Search(t.Context(), Exact, "lego.wtf")
require.NoError(t, err)
diff --git a/providers/dns/constellix/internal/txtrecords_test.go b/providers/dns/constellix/internal/txtrecords_test.go
index ee4d20bf2..54d10dc38 100644
--- a/providers/dns/constellix/internal/txtrecords_test.go
+++ b/providers/dns/constellix/internal/txtrecords_test.go
@@ -2,37 +2,19 @@ package internal
import (
"encoding/json"
- "io"
- "net/http"
"os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTxtRecordService_Create(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/12345/records/txt", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/records-Create.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /v1/domains/12345/records/txt", servermock.ResponseFromFixture("records-Create.json"),
+ servermock.CheckRequestJSONBody(`{"name":""}`)).
+ Build(t)
records, err := client.TxtRecords.Create(t.Context(), 12345, RecordRequest{})
require.NoError(t, err)
@@ -47,27 +29,9 @@ func TestTxtRecordService_Create(t *testing.T) {
}
func TestTxtRecordService_GetAll(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/12345/records/txt", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/records-GetAll.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /v1/domains/12345/records/txt", servermock.ResponseFromFixture("records-GetAll.json")).
+ Build(t)
records, err := client.TxtRecords.GetAll(t.Context(), 12345)
require.NoError(t, err)
@@ -82,27 +46,9 @@ func TestTxtRecordService_GetAll(t *testing.T) {
}
func TestTxtRecordService_Get(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/records-Get.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /v1/domains/12345/records/txt/6789", servermock.ResponseFromFixture("records-Get.json")).
+ Build(t)
record, err := client.TxtRecords.Get(t.Context(), 12345, 6789)
require.NoError(t, err)
@@ -130,20 +76,10 @@ func TestTxtRecordService_Get(t *testing.T) {
}
func TestTxtRecordService_Update(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPut {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- _, err := rw.Write([]byte(`{"success":"Record updated successfully"}`))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("PUT /v1/domains/12345/records/txt/6789",
+ servermock.RawStringResponse(`{"success":"Record updated successfully"}`)).
+ Build(t)
msg, err := client.TxtRecords.Update(t.Context(), 12345, 6789, RecordRequest{})
require.NoError(t, err)
@@ -153,20 +89,10 @@ func TestTxtRecordService_Update(t *testing.T) {
}
func TestTxtRecordService_Delete(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/12345/records/txt/6789", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- _, err := rw.Write([]byte(`{"success":"Record deleted successfully"}`))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("DELETE /v1/domains/12345/records/txt/6789",
+ servermock.RawStringResponse(`{"success":"Record deleted successfully"}`)).
+ Build(t)
msg, err := client.TxtRecords.Delete(t.Context(), 12345, 6789)
require.NoError(t, err)
@@ -176,27 +102,9 @@ func TestTxtRecordService_Delete(t *testing.T) {
}
func TestTxtRecordService_Search(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/12345/records/txt/search", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/records-Search.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /v1/domains/12345/records/txt/search", servermock.ResponseFromFixture("records-Search.json")).
+ Build(t)
records, err := client.TxtRecords.Search(t.Context(), 12345, Exact, "test")
require.NoError(t, err)
diff --git a/providers/dns/corenetworks/internal/client_test.go b/providers/dns/corenetworks/internal/client_test.go
index ec6de452e..ca5c81a65 100644
--- a/providers/dns/corenetworks/internal/client_test.go
+++ b/providers/dns/corenetworks/internal/client_test.go
@@ -1,112 +1,34 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("user", "secret")
- client.baseURL, _ = url.Parse(server.URL)
- client.HTTPClient = server.Client()
-
- return client, mux
-}
-
-func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf(`unsupported method: %s`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- rw.WriteHeader(statusCode)
-
- if statusCode == http.StatusNoContent {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, fmt.Sprintf(`message %v`, err), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, fmt.Sprintf(`message %v`, err), http.StatusInternalServerError)
- return
- }
- }
-}
-
-func testHandlerAuth(method string, statusCode int, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- rw.WriteHeader(statusCode)
-
- if statusCode == http.StatusNoContent {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
- }
-}
-
-func TestClient_CreateAuthenticationToken(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/auth/token", testHandlerAuth(http.MethodPost, http.StatusOK, "auth.json"))
-
- ctx := t.Context()
-
- token, err := client.CreateAuthenticationToken(ctx)
- require.NoError(t, err)
-
- expected := &Token{
- Token: "authsecret",
- Expires: 123,
- }
- assert.Equal(t, expected, token)
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestClient_ListZone(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/dnszones/", testHandler(http.MethodGet, http.StatusOK, "ListZone.json"))
+ client := mockBuilder().
+ Route("GET /dnszones/",
+ servermock.ResponseFromFixture("ListZone.json")).
+ Build(t)
ctx := t.Context()
@@ -122,13 +44,12 @@ func TestClient_ListZone(t *testing.T) {
}
func TestClient_GetZoneDetails(t *testing.T) {
- client, mux := setupTest(t)
+ client := mockBuilder().
+ Route("GET /dnszones/example.com",
+ servermock.ResponseFromFixture("GetZoneDetails.json")).
+ Build(t)
- mux.HandleFunc("/dnszones/example.com", testHandler(http.MethodGet, http.StatusOK, "GetZoneDetails.json"))
-
- ctx := t.Context()
-
- zone, err := client.GetZoneDetails(ctx, "example.com")
+ zone, err := client.GetZoneDetails(t.Context(), "example.com")
require.NoError(t, err)
expected := &ZoneDetails{
@@ -142,13 +63,12 @@ func TestClient_GetZoneDetails(t *testing.T) {
}
func TestClient_ListRecords(t *testing.T) {
- client, mux := setupTest(t)
+ client := mockBuilder().
+ Route("GET /dnszones/example.com/records/",
+ servermock.ResponseFromFixture("ListRecords.json")).
+ Build(t)
- mux.HandleFunc("/dnszones/example.com/records/", testHandler(http.MethodGet, http.StatusOK, "ListRecords.json"))
-
- ctx := t.Context()
-
- records, err := client.ListRecords(ctx, "example.com")
+ records, err := client.ListRecords(t.Context(), "example.com")
require.NoError(t, err)
expected := []Record{
@@ -176,38 +96,35 @@ func TestClient_ListRecords(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/dnszones/example.com/records/", testHandler(http.MethodPost, http.StatusNoContent, ""))
-
- ctx := t.Context()
+ client := mockBuilder().
+ Route("POST /dnszones/example.com/records/",
+ servermock.Noop().WithStatusCode(http.StatusNoContent)).
+ Build(t)
record := Record{Name: "www", TTL: 3600, Type: "A", Data: "127.0.0.1"}
- err := client.AddRecord(ctx, "example.com", record)
+ err := client.AddRecord(t.Context(), "example.com", record)
require.NoError(t, err)
}
func TestClient_DeleteRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/dnszones/example.com/records/delete", testHandler(http.MethodPost, http.StatusNoContent, ""))
-
- ctx := t.Context()
+ client := mockBuilder().
+ Route("POST /dnszones/example.com/records/delete",
+ servermock.Noop().WithStatusCode(http.StatusNoContent)).
+ Build(t)
record := Record{Name: "www", Type: "A", Data: "127.0.0.1"}
- err := client.DeleteRecords(ctx, "example.com", record)
+ err := client.DeleteRecords(t.Context(), "example.com", record)
require.NoError(t, err)
}
func TestClient_CommitRecords(t *testing.T) {
- client, mux := setupTest(t)
+ client := mockBuilder().
+ Route("POST /dnszones/example.com/records/commit",
+ servermock.Noop().WithStatusCode(http.StatusNoContent)).
+ Build(t)
- mux.HandleFunc("/dnszones/example.com/records/commit", testHandler(http.MethodPost, http.StatusNoContent, ""))
-
- ctx := t.Context()
-
- err := client.CommitRecords(ctx, "example.com")
+ err := client.CommitRecords(t.Context(), "example.com")
require.NoError(t, err)
}
diff --git a/providers/dns/corenetworks/internal/identity_test.go b/providers/dns/corenetworks/internal/identity_test.go
new file mode 100644
index 000000000..b5e05ed3f
--- /dev/null
+++ b/providers/dns/corenetworks/internal/identity_test.go
@@ -0,0 +1,24 @@
+package internal
+
+import (
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestClient_CreateAuthenticationToken(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /auth/token", servermock.ResponseFromFixture("auth.json")).
+ Build(t)
+
+ token, err := client.CreateAuthenticationToken(t.Context())
+ require.NoError(t, err)
+
+ expected := &Token{
+ Token: "authsecret",
+ Expires: 123,
+ }
+ assert.Equal(t, expected, token)
+}
diff --git a/providers/dns/cpanel/internal/cpanel/client_test.go b/providers/dns/cpanel/internal/cpanel/client_test.go
index 78c45e82d..533d1130d 100644
--- a/providers/dns/cpanel/internal/cpanel/client_test.go
+++ b/providers/dns/cpanel/internal/cpanel/client_test.go
@@ -1,58 +1,38 @@
package cpanel
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient(server.URL, "user", "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(http.StatusOK)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient(server.URL, "user", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("cpanel user:secret"))
}
func TestClient_FetchZoneInformation(t *testing.T) {
- client := setupTest(t, "/execute/DNS/parse_zone", "zone-info.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/parse_zone",
+ servermock.ResponseFromFixture("zone-info.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("zone", "example.com")).
+ Build(t)
zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com")
require.NoError(t, err)
@@ -70,16 +50,27 @@ func TestClient_FetchZoneInformation(t *testing.T) {
}
func TestClient_FetchZoneInformation_error(t *testing.T) {
- client := setupTest(t, "/execute/DNS/parse_zone", "zone-info_error.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/parse_zone",
+ servermock.ResponseFromFixture("zone-info_error.json")).
+ Build(t)
zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com")
- require.Error(t, err)
+ require.EqualError(t, err, "error(0): You do not control a DNS zone named example.com.: a, b, c")
assert.Nil(t, zoneInfo)
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/mass_edit_zone",
+ servermock.ResponseFromFixture("update-zone.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("zone", "example.com").
+ With("add", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"]}`).
+ With("serial", "123456").
+ With("zone", "example.com")).
+ Build(t)
record := shared.Record{
DName: "example",
@@ -97,7 +88,10 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/mass_edit_zone",
+ servermock.ResponseFromFixture("update-zone_error.json")).
+ Build(t)
record := shared.Record{
DName: "example",
@@ -113,7 +107,14 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_EditRecord(t *testing.T) {
- client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/mass_edit_zone",
+ servermock.ResponseFromFixture("update-zone.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("edit", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"],"line_index":9}`).
+ With("serial", "123456").
+ With("zone", "example.com")).
+ Build(t)
record := shared.Record{
LineIndex: 9,
@@ -132,7 +133,10 @@ func TestClient_EditRecord(t *testing.T) {
}
func TestClient_EditRecord_error(t *testing.T) {
- client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/mass_edit_zone",
+ servermock.ResponseFromFixture("update-zone_error.json")).
+ Build(t)
record := shared.Record{
LineIndex: 9,
@@ -149,7 +153,14 @@ func TestClient_EditRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/mass_edit_zone",
+ servermock.ResponseFromFixture("update-zone.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("remove", "0").
+ With("serial", "123456").
+ With("zone", "example.com")).
+ Build(t)
zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0)
require.NoError(t, err)
@@ -160,7 +171,10 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "/execute/DNS/mass_edit_zone", "update-zone_error.json")
+ client := mockBuilder().
+ Route("GET /execute/DNS/mass_edit_zone",
+ servermock.ResponseFromFixture("update-zone_error.json")).
+ Build(t)
zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0)
require.Error(t, err)
diff --git a/providers/dns/cpanel/internal/whm/client_test.go b/providers/dns/cpanel/internal/whm/client_test.go
index 536417666..47686bf09 100644
--- a/providers/dns/cpanel/internal/whm/client_test.go
+++ b/providers/dns/cpanel/internal/whm/client_test.go
@@ -1,58 +1,39 @@
package whm
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/go-acme/lego/v4/providers/dns/cpanel/internal/shared"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient(server.URL, "user", "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(http.StatusOK)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient(server.URL, "user", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("whm user:secret"))
}
func TestClient_FetchZoneInformation(t *testing.T) {
- client := setupTest(t, "/json-api/parse_dns_zone", "zone-info.json")
+ client := mockBuilder().
+ Route("GET /json-api/parse_dns_zone",
+ servermock.ResponseFromFixture("zone-info.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("api.version", "1").
+ With("zone", "example.com")).
+ Build(t)
zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com")
require.NoError(t, err)
@@ -70,7 +51,10 @@ func TestClient_FetchZoneInformation(t *testing.T) {
}
func TestClient_FetchZoneInformation_error(t *testing.T) {
- client := setupTest(t, "/json-api/parse_dns_zone", "zone-info_error.json")
+ client := mockBuilder().
+ Route("GET /json-api/parse_dns_zone",
+ servermock.ResponseFromFixture("zone-info_error.json")).
+ Build(t)
zoneInfo, err := client.FetchZoneInformation(t.Context(), "example.com")
require.Error(t, err)
@@ -79,7 +63,15 @@ func TestClient_FetchZoneInformation_error(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json")
+ client := mockBuilder().
+ Route("GET /json-api/mass_edit_dns_zone",
+ servermock.ResponseFromFixture("update-zone.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("add", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"]}`).
+ With("api.version", "1").
+ With("serial", "123456").
+ With("zone", "example.com")).
+ Build(t)
record := shared.Record{
DName: "example",
@@ -97,7 +89,10 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json")
+ client := mockBuilder().
+ Route("GET /json-api/mass_edit_dns_zone",
+ servermock.ResponseFromFixture("update-zone_error.json")).
+ Build(t)
record := shared.Record{
DName: "example",
@@ -113,7 +108,15 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_EditRecord(t *testing.T) {
- client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json")
+ client := mockBuilder().
+ Route("GET /json-api/mass_edit_dns_zone",
+ servermock.ResponseFromFixture("update-zone.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("edit", `{"dname":"example","ttl":14400,"record_type":"TXT","data":["string1","string2"],"line_index":9}`).
+ With("api.version", "1").
+ With("serial", "123456").
+ With("zone", "example.com")).
+ Build(t)
record := shared.Record{
LineIndex: 9,
@@ -132,7 +135,10 @@ func TestClient_EditRecord(t *testing.T) {
}
func TestClient_EditRecord_error(t *testing.T) {
- client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json")
+ client := mockBuilder().
+ Route("GET /json-api/mass_edit_dns_zone",
+ servermock.ResponseFromFixture("update-zone_error.json")).
+ Build(t)
record := shared.Record{
LineIndex: 9,
@@ -149,7 +155,15 @@ func TestClient_EditRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone.json")
+ client := mockBuilder().
+ Route("GET /json-api/mass_edit_dns_zone",
+ servermock.ResponseFromFixture("update-zone.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("remove", "0").
+ With("api.version", "1").
+ With("serial", "123456").
+ With("zone", "example.com")).
+ Build(t)
zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0)
require.NoError(t, err)
@@ -160,7 +174,10 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "/json-api/mass_edit_dns_zone", "update-zone_error.json")
+ client := mockBuilder().
+ Route("GET /json-api/mass_edit_dns_zone",
+ servermock.ResponseFromFixture("update-zone_error.json")).
+ Build(t)
zoneSerial, err := client.DeleteRecord(t.Context(), 123456, "example.com", 0)
require.Error(t, err)
diff --git a/providers/dns/derak/internal/client_test.go b/providers/dns/derak/internal/client_test.go
index 20dea0015..322a7f48c 100644
--- a/providers/dns/derak/internal/client_test.go
+++ b/providers/dns/derak/internal/client_test.go
@@ -1,80 +1,37 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
"time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("secret")
client.baseURL, _ = url.Parse(server.URL)
client.zoneEndpoint = server.URL
client.HTTPClient = server.Client()
- return client, mux
+ return client, nil
}
-func testHandler(method string, statusCode int, filename string) func(rw http.ResponseWriter, req *http.Request) {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- username, password, ok := req.BasicAuth()
- if !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if username != "api" {
- http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized)
- return
- }
-
- if password != "secret" {
- http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("api", "secret"))
}
func TestGetRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
- testHandler(http.MethodGet, http.StatusOK, "records-GET.json"))
+ client := mockBuilder().
+ Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
+ servermock.ResponseFromFixture("records-GET.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", &GetRecordsParameters{DNSType: "TXT", Content: `"test"'`})
require.NoError(t, err)
@@ -134,20 +91,21 @@ func TestGetRecords(t *testing.T) {
}
func TestGetRecords_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
- testHandler(http.MethodGet, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetRecords(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", &GetRecordsParameters{DNSType: "TXT", Content: `"test"'`})
require.Error(t, err)
}
func TestGetRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/812bee17a0b440b0bd5ee099a78b839c",
- testHandler(http.MethodGet, http.StatusOK, "record-GET.json"))
+ client := mockBuilder().
+ Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/812bee17a0b440b0bd5ee099a78b839c",
+ servermock.ResponseFromFixture("record-GET.json")).
+ Build(t)
record, err := client.GetRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "812bee17a0b440b0bd5ee099a78b839c")
require.NoError(t, err)
@@ -163,20 +121,22 @@ func TestGetRecord(t *testing.T) {
}
func TestGetRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/812bee17a0b440b0bd5ee099a78b839c",
- testHandler(http.MethodGet, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("GET /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "812bee17a0b440b0bd5ee099a78b839c")
require.Error(t, err)
}
func TestCreateRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
- testHandler(http.MethodPut, http.StatusCreated, "record-PUT.json"))
+ client := mockBuilder().
+ Route("PUT /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
+ servermock.ResponseFromFixture("record-PUT.json").
+ WithStatusCode(http.StatusCreated)).
+ Build(t)
r := Record{
Type: "TXT",
@@ -199,10 +159,11 @@ func TestCreateRecord(t *testing.T) {
}
func TestCreateRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
- testHandler(http.MethodPut, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("PUT /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
r := Record{
Type: "TXT",
@@ -216,10 +177,10 @@ func TestCreateRecord_error(t *testing.T) {
}
func TestEditRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2",
- testHandler(http.MethodPatch, http.StatusOK, "record-PATCH.json"))
+ client := mockBuilder().
+ Route("PATCH /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2",
+ servermock.ResponseFromFixture("record-PATCH.json")).
+ Build(t)
record, err := client.EditRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "eebc813de2f94d67b09d91e10e2d65c2", Record{
Content: "foo",
@@ -237,10 +198,11 @@ func TestEditRecord(t *testing.T) {
}
func TestEditRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2",
- testHandler(http.MethodPatch, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("PATCH /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/eebc813de2f94d67b09d91e10e2d65c2",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.EditRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "eebc813de2f94d67b09d91e10e2d65c2", Record{
Content: "foo",
@@ -249,29 +211,33 @@ func TestEditRecord_error(t *testing.T) {
}
func TestDeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df",
- testHandler(http.MethodDelete, http.StatusOK, "record-DELETE.json"))
+ client := mockBuilder().
+ Route("DELETE /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df",
+ servermock.ResponseFromFixture("record-DELETE.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "653464211b7447a1bee6b8fcb9fb86df")
require.NoError(t, err)
}
func TestDeleteRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df",
- testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("DELETE /zones/47c0ecf6c91243308c649ad1d2d618dd/dnsrecords/653464211b7447a1bee6b8fcb9fb86df",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "47c0ecf6c91243308c649ad1d2d618dd", "653464211b7447a1bee6b8fcb9fb86df")
require.Error(t, err)
}
func TestGetZones(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", testHandler(http.MethodGet, http.StatusOK, "service-cdn-zones.json"))
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().
+ WithBasicAuth("api", "secret"),
+ ).
+ Route("GET /", servermock.ResponseFromFixture("service-cdn-zones.json")).
+ Build(t)
zones, err := client.GetZones(t.Context())
require.NoError(t, err)
@@ -302,9 +268,10 @@ func TestGetZones(t *testing.T) {
}
func TestGetZones_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("GET /", servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetZones(t.Context())
require.Error(t, err)
diff --git a/providers/dns/digitalocean/digitalocean_test.go b/providers/dns/digitalocean/digitalocean_test.go
index bfd2d68c0..a01906812 100644
--- a/providers/dns/digitalocean/digitalocean_test.go
+++ b/providers/dns/digitalocean/digitalocean_test.go
@@ -1,36 +1,30 @@
package digitalocean
import (
- "bytes"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
- "github.com/stretchr/testify/assert"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
var envTest = tester.NewEnvTest(EnvAuthToken)
-func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) {
- t.Helper()
+func mockProvider() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.AuthToken = "asdf1234"
+ config.BaseURL = server.URL
+ config.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- config := NewDefaultConfig()
- config.AuthToken = "asdf1234"
- config.BaseURL = server.URL
- config.HTTPClient = server.Client()
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- return provider, mux
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().
+ WithJSONHeaders().
+ With("Authorization", "Bearer asdf1234"))
}
func TestNewDNSProvider(t *testing.T) {
@@ -111,26 +105,9 @@ func TestNewDNSProviderConfig(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodPost, r.Method, "method")
-
- assert.Equal(t, "application/json", r.Header.Get("Accept"), "Accept")
- assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type")
- assert.Equal(t, "Bearer asdf1234", r.Header.Get("Authorization"), "Authorization")
-
- reqBody, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- expectedReqBody := `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`
- assert.Equal(t, expectedReqBody, string(bytes.TrimSpace(reqBody)))
-
- w.WriteHeader(http.StatusCreated)
- _, err = fmt.Fprintf(w, `{
+ provider := mockProvider().
+ Route("POST /v2/domains/example.com/records",
+ servermock.RawStringResponse(`{
"domain_record": {
"id": 1234567,
"type": "TXT",
@@ -140,31 +117,21 @@ func TestDNSProvider_Present(t *testing.T) {
"port": null,
"weight": null
}
- }`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ }`).
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBody(`{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`)).
+ Build(t)
err := provider.Present("example.com", "", "foobar")
require.NoError(t, err)
}
func TestDNSProvider_CleanUp(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/v2/domains/example.com/records/1234567", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodDelete, r.Method, "method")
-
- assert.Equal(t, "/v2/domains/example.com/records/1234567", r.URL.Path, "Path")
-
- assert.Equal(t, "application/json", r.Header.Get("Accept"), "Accept")
- assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type")
- assert.Equal(t, "Bearer asdf1234", r.Header.Get("Authorization"), "Authorization")
-
- w.WriteHeader(http.StatusNoContent)
- })
+ provider := mockProvider().
+ Route("DELETE /v2/domains/example.com/records/1234567",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
provider.recordIDsMu.Lock()
provider.recordIDs["token"] = 1234567
diff --git a/providers/dns/digitalocean/internal/client_test.go b/providers/dns/digitalocean/internal/client_test.go
index 171601438..65ce5dfaa 100644
--- a/providers/dns/digitalocean/internal/client_test.go
+++ b/providers/dns/digitalocean/internal/client_test.go
@@ -1,94 +1,35 @@
package internal
import (
- "bytes"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"))
+ client.BaseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"))
- client.BaseURL, _ = url.Parse(server.URL)
-
- mux.HandleFunc(pattern, handler)
-
- return client
-}
-
-func checkHeader(req *http.Request, name, value string) error {
- val := req.Header.Get(name)
- if val != value {
- return fmt.Errorf("invalid header value, got: %s want %s", val, value)
- }
- return nil
-}
-
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer secret"))
}
func TestClient_AddTxtRecord(t *testing.T) {
- client := setupTest(t, "/v2/domains/example.com/records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- err := checkHeader(req, "Accept", "application/json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- err = checkHeader(req, "Content-Type", "application/json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- err = checkHeader(req, "Authorization", "Bearer secret")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- reqBody, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- expectedReqBody := `{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`
- if expectedReqBody != string(bytes.TrimSpace(reqBody)) {
- http.Error(rw, fmt.Sprintf("unexpected request body: %s", string(bytes.TrimSpace(reqBody))), http.StatusBadRequest)
- return
- }
-
- rw.WriteHeader(http.StatusCreated)
- writeFixture(rw, "domains-records_POST.json")
- })
+ client := mockBuilder().
+ Route("POST /v2/domains/example.com/records",
+ servermock.ResponseFromFixture("domains-records_POST.json").
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBody(`{"type":"TXT","name":"_acme-challenge.example.com.","data":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":30}`)).
+ Build(t)
record := Record{
Type: "TXT",
@@ -112,26 +53,11 @@ func TestClient_AddTxtRecord(t *testing.T) {
}
func TestClient_RemoveTxtRecord(t *testing.T) {
- client := setupTest(t, "/v2/domains/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- err := checkHeader(req, "Accept", "application/json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- err = checkHeader(req, "Authorization", "Bearer secret")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(http.StatusNoContent)
- })
+ client := mockBuilder().
+ Route("DELETE /v2/domains/example.com/records/1234567",
+ servermock.ResponseFromFixture("domains-records_POST.json").
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.RemoveTxtRecord(t.Context(), "example.com", 1234567)
require.NoError(t, err)
diff --git a/providers/dns/directadmin/internal/client_test.go b/providers/dns/directadmin/internal/client_test.go
index 6da73da65..759a7fb4e 100644
--- a/providers/dns/directadmin/internal/client_test.go
+++ b/providers/dns/directadmin/internal/client_test.go
@@ -1,88 +1,48 @@
package internal
import (
- "encoding/json"
"fmt"
- "io"
"net/http"
"net/http/httptest"
- "net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, _ := NewClient(server.URL, "user", "secret")
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client, _ := NewClient(server.URL, "user", "secret")
- client.HTTPClient = server.Client()
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded())
}
-func newJSONErrorf(reason string, a ...any) string {
- err := APIError{
+func newAPIError(reason string, a ...any) APIError {
+ return APIError{
Message: "Cannot View Dns Record",
Result: fmt.Sprintf(reason, a...),
}
-
- data, _ := json.Marshal(err)
-
- return string(data)
-}
-
-func testHandler(kv map[string]string) func(rw http.ResponseWriter, req *http.Request) {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- domain := req.URL.Query().Get("domain")
- if domain != "example.com" {
- http.Error(rw, newJSONErrorf("invalid domain: %s", domain), http.StatusUnauthorized)
- return
- }
-
- data, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- values, err := url.ParseQuery(string(data))
- if err != nil {
- http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- for k, v := range kv {
- actual := values.Get(k)
- if v != actual {
- http.Error(rw, newJSONErrorf("invalid %q: %s", k, actual), http.StatusBadRequest)
- return
- }
- }
- }
}
func TestClient_SetRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- kv := map[string]string{
- "action": "add",
- "name": "foo",
- "type": "TXT",
- "value": "txtTXTtxt",
- "ttl": "123",
- }
-
- mux.HandleFunc("/CMD_API_DNS_CONTROL", testHandler(kv))
+ client := mockBuilder().
+ Route("POST /CMD_API_DNS_CONTROL", nil,
+ servermock.CheckQueryParameter().Strict().
+ With("domain", "example.com").
+ With("json", "yes"),
+ servermock.CheckForm().UsePostForm().Strict().
+ With("action", "add").
+ With("name", "foo").
+ With("type", "TXT").
+ With("value", "txtTXTtxt").
+ With("ttl", "123"),
+ ).
+ Build(t)
record := Record{
Name: "foo",
@@ -96,11 +56,11 @@ func TestClient_SetRecord(t *testing.T) {
}
func TestClient_SetRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/CMD_API_DNS_CONTROL", func(rw http.ResponseWriter, req *http.Request) {
- http.Error(rw, newJSONErrorf("OOPS"), http.StatusInternalServerError)
- })
+ client := mockBuilder().
+ Route("POST /CMD_API_DNS_CONTROL",
+ servermock.JSONEncode(newAPIError("OOPS")).
+ WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
record := Record{
Name: "foo",
@@ -114,17 +74,18 @@ func TestClient_SetRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- kv := map[string]string{
- "action": "delete",
- "name": "foo",
- "type": "TXT",
- "value": "txtTXTtxt",
- "ttl": "",
- }
-
- mux.HandleFunc("/CMD_API_DNS_CONTROL", testHandler(kv))
+ client := mockBuilder().
+ Route("POST /CMD_API_DNS_CONTROL", nil,
+ servermock.CheckQueryParameter().Strict().
+ With("domain", "example.com").
+ With("json", "yes"),
+ servermock.CheckForm().UsePostForm().Strict().
+ With("action", "delete").
+ With("name", "foo").
+ With("type", "TXT").
+ With("value", "txtTXTtxt"),
+ ).
+ Build(t)
record := Record{
Name: "foo",
@@ -137,11 +98,11 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/CMD_API_DNS_CONTROL", func(rw http.ResponseWriter, req *http.Request) {
- http.Error(rw, newJSONErrorf("OOPS"), http.StatusInternalServerError)
- })
+ client := mockBuilder().
+ Route("POST /CMD_API_DNS_CONTROL",
+ servermock.JSONEncode(newAPIError("OOPS")).
+ WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
record := Record{
Name: "foo",
diff --git a/providers/dns/dnshomede/internal/client_test.go b/providers/dns/dnshomede/internal/client_test.go
index 710e2c72e..6e1593fe7 100644
--- a/providers/dns/dnshomede/internal/client_test.go
+++ b/providers/dns/dnshomede/internal/client_test.go
@@ -2,33 +2,32 @@ package internal
import (
"fmt"
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, credentials map[string]string, handler http.HandlerFunc) *Client {
- t.Helper()
+func setupClient(credentials map[string]string) func(server *httptest.Server) (*Client, error) {
+ return func(server *httptest.Server) (*Client, error) {
+ client := NewClient(credentials)
+ client.HTTPClient = server.Client()
+ client.baseURL = server.URL
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", handler)
-
- client := NewClient(credentials)
- client.HTTPClient = server.Client()
- client.baseURL = server.URL
-
- return client
+ return client, nil
+ }
}
func TestClient_Add(t *testing.T) {
txtValue := "123456789012"
- client := setupTest(t, map[string]string{"example.org": "secret"}, handlerMock(addAction, txtValue))
+ client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.org": "secret"})).
+ Route("POST /",
+ servermock.RawStringResponse(fmt.Sprintf("%s %s", successCode, txtValue)),
+ servermock.CheckQueryParameter().Strict().
+ With("acme", addAction).With("txt", txtValue)).
+ Build(t)
err := client.Add(t.Context(), "example.org", txtValue)
require.NoError(t, err)
@@ -37,16 +36,27 @@ func TestClient_Add(t *testing.T) {
func TestClient_Add_error(t *testing.T) {
txtValue := "123456789012"
- client := setupTest(t, map[string]string{"example.com": "secret"}, handlerMock(addAction, txtValue))
+ client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.com": "secret"})).
+ Route("POST /",
+ servermock.RawStringResponse(fmt.Sprintf("%s %s", successCode, txtValue)),
+ servermock.CheckQueryParameter().Strict().
+ With("acme", addAction).With("txt", txtValue)).
+ Build(t)
err := client.Add(t.Context(), "example.org", txtValue)
- require.Error(t, err)
+
+ require.EqualError(t, err, "domain example.org not found in credentials, check your credentials map")
}
func TestClient_Remove(t *testing.T) {
txtValue := "ABCDEFGHIJKL"
- client := setupTest(t, map[string]string{"example.org": "secret"}, handlerMock(removeAction, txtValue))
+ client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.org": "secret"})).
+ Route("POST /",
+ servermock.RawStringResponse(fmt.Sprintf("%s %s", successCode, txtValue)),
+ servermock.CheckQueryParameter().Strict().
+ With("acme", removeAction).With("txt", txtValue)).
+ Build(t)
err := client.Remove(t.Context(), "example.org", txtValue)
require.NoError(t, err)
@@ -55,34 +65,45 @@ func TestClient_Remove(t *testing.T) {
func TestClient_Remove_error(t *testing.T) {
txtValue := "ABCDEFGHIJKL"
- client := setupTest(t, map[string]string{"example.com": "secret"}, handlerMock(removeAction, txtValue))
+ testCases := []struct {
+ desc string
+ hostname string
+ response string
+ expected string
+ }{
+ {
+ desc: "response error - txt",
+ hostname: "example.com",
+ response: "error - no valid acme txt record",
+ expected: "error - no valid acme txt record",
+ },
+ {
+ desc: "response error - acme",
+ hostname: "example.com",
+ response: "nochg 1234:1234:1234:1234:1234:1234:1234:1234",
+ expected: "nochg 1234:1234:1234:1234:1234:1234:1234:1234",
+ },
+ {
+ desc: "credential error",
+ hostname: "example.org",
+ response: fmt.Sprintf("%s %s", successCode, txtValue),
+ expected: "domain example.org not found in credentials, check your credentials map",
+ },
+ }
- err := client.Remove(t.Context(), "example.org", txtValue)
- require.Error(t, err)
-}
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ t.Parallel()
-func handlerMock(action, value string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- rw.WriteHeader(http.StatusOK)
+ client := servermock.NewBuilder[*Client](setupClient(map[string]string{"example.com": "secret"})).
+ Route("POST /",
+ servermock.RawStringResponse(test.response),
+ servermock.CheckQueryParameter().Strict().
+ With("acme", removeAction).With("txt", txtValue)).
+ Build(t)
- query := req.URL.Query()
-
- if query.Get("acme") != action {
- _, _ = rw.Write([]byte("nochg 1234:1234:1234:1234:1234:1234:1234:1234"))
- return
- }
-
- txtValue := query.Get("txt")
- if len(txtValue) < 12 {
- _, _ = rw.Write([]byte("error - no valid acme txt record"))
- return
- }
-
- if txtValue != value {
- http.Error(rw, fmt.Sprintf("got: %q, expected: %q", txtValue, value), http.StatusBadRequest)
- return
- }
-
- _, _ = fmt.Fprintf(rw, "%s %s", successCode, txtValue)
+ err := client.Remove(t.Context(), test.hostname, txtValue)
+ require.EqualError(t, err, test.expected)
+ })
}
}
diff --git a/providers/dns/dnshomede/internal/readme.md b/providers/dns/dnshomede/internal/readme.md
index 014b062a1..622c4354d 100644
--- a/providers/dns/dnshomede/internal/readme.md
+++ b/providers/dns/dnshomede/internal/readme.md
@@ -16,7 +16,7 @@ Always returns StatusOK (200)
If the API call works the first word of the response body is `successfully`.
-If an error encoured the response body is `error - `.
+If an error occurs the response body is `error - `.
Can be a POST or a GET.
@@ -35,6 +35,6 @@ Always returns StatusOK (200)
If the API call works the first word of the response body is `successfully`.
-If an error encoured the response body is `error - `.
+If an error occurs the response body is `error - `.
Can be a POST or a GET.
diff --git a/providers/dns/dnsmadeeasy/internal/client.go b/providers/dns/dnsmadeeasy/internal/client.go
index 491d5fd98..cb6f9d2cb 100644
--- a/providers/dns/dnsmadeeasy/internal/client.go
+++ b/providers/dns/dnsmadeeasy/internal/client.go
@@ -15,6 +15,7 @@ import (
"strconv"
"time"
+ "github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)
@@ -57,10 +58,8 @@ func NewClient(apiKey, apiSecret string) (*Client, error) {
func (c *Client) GetDomain(ctx context.Context, authZone string) (*Domain, error) {
endpoint := c.BaseURL.JoinPath("dns", "managed", "name")
- domainName := authZone[0 : len(authZone)-1]
-
query := endpoint.Query()
- query.Set("domainname", domainName)
+ query.Set("domainname", dns01.UnFqdn(authZone))
endpoint.RawQuery = query.Encode()
req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
diff --git a/providers/dns/dnsmadeeasy/internal/client_test.go b/providers/dns/dnsmadeeasy/internal/client_test.go
index 721214693..f302c8d9b 100644
--- a/providers/dns/dnsmadeeasy/internal/client_test.go
+++ b/providers/dns/dnsmadeeasy/internal/client_test.go
@@ -2,14 +2,132 @@ package internal
import (
"net/http"
+ "net/http/httptest"
+ "net/url"
"testing"
"time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func Test_sign(t *testing.T) {
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("key", "secret")
+ if err != nil {
+ return nil, err
+ }
+
+ client.BaseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With("x-dnsme-apiKey", "key").
+ WithRegexp("x-dnsme-requestDate", `\w+, \d+ \w+ \d+ \d+:\d+:\d+ UTC`).
+ WithRegexp("x-dnsme-hmac", `[a-z0-9]+`),
+ )
+}
+
+func TestClient_GetDomain(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /dns/managed/name",
+ servermock.RawStringResponse(`{"id": 1, "name": "foo"}`),
+ servermock.CheckQueryParameter().Strict().
+ With("domainname", "example.com")).
+ Build(t)
+
+ domain, err := client.GetDomain(t.Context(), "example.com.")
+ require.NoError(t, err)
+
+ expected := &Domain{
+ ID: 1,
+ Name: "foo",
+ }
+
+ assert.Equal(t, expected, domain)
+}
+
+func TestClient_GetRecords(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /dns/managed/1/records",
+ servermock.ResponseFromFixture("get_records.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("recordName", "foo").
+ With("type", "TXT"),
+ ).
+ Build(t)
+
+ domain := &Domain{ID: 1, Name: "foo"}
+
+ records, err := client.GetRecords(t.Context(), domain, "foo", "TXT")
+ require.NoError(t, err)
+
+ expected := []Record{
+ {
+ ID: 1,
+ Type: "TXT",
+ Name: "foo",
+ Value: "aaa",
+ TTL: 60,
+ SourceID: 123,
+ },
+ {
+ ID: 2,
+ Type: "TXT",
+ Name: "bar",
+ Value: "bbb",
+ TTL: 120,
+ SourceID: 456,
+ },
+ }
+
+ assert.Equal(t, &expected, records)
+}
+
+func TestClient_CreateRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /dns/managed/1/records", nil,
+ servermock.CheckRequestJSONBodyFromFile("create_record-request.json")).
+ Build(t)
+
+ domain := &Domain{ID: 1, Name: "foo"}
+
+ record := &Record{
+ ID: 1,
+ Type: "TXT",
+ Name: "foo",
+ Value: "aaa",
+ TTL: 60,
+ SourceID: 123,
+ }
+
+ err := client.CreateRecord(t.Context(), domain, record)
+ require.NoError(t, err)
+}
+
+func TestClient_DeleteRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("DELETE /dns/managed/123/records/1", nil).
+ Build(t)
+
+ record := Record{
+ ID: 1,
+ Type: "TXT",
+ Name: "foo",
+ Value: "aaa",
+ TTL: 60,
+ SourceID: 123,
+ }
+
+ err := client.DeleteRecord(t.Context(), record)
+ require.NoError(t, err)
+}
+
+func TestClient_sign(t *testing.T) {
apiKey := "key"
client := Client{apiKey: apiKey, apiSecret: "secret"}
diff --git a/providers/dns/dnsmadeeasy/internal/fixtures/create_record-request.json b/providers/dns/dnsmadeeasy/internal/fixtures/create_record-request.json
new file mode 100644
index 000000000..9a08b6544
--- /dev/null
+++ b/providers/dns/dnsmadeeasy/internal/fixtures/create_record-request.json
@@ -0,0 +1,8 @@
+{
+ "id": 1,
+ "type": "TXT",
+ "name": "foo",
+ "value": "aaa",
+ "ttl": 60,
+ "sourceId": 123
+}
diff --git a/providers/dns/dnsmadeeasy/internal/fixtures/get_records.json b/providers/dns/dnsmadeeasy/internal/fixtures/get_records.json
new file mode 100644
index 000000000..5667e5e1d
--- /dev/null
+++ b/providers/dns/dnsmadeeasy/internal/fixtures/get_records.json
@@ -0,0 +1,20 @@
+{
+ "data": [
+ {
+ "id": 1,
+ "type": "TXT",
+ "name": "foo",
+ "value": "aaa",
+ "ttl": 60,
+ "sourceId": 123
+ },
+ {
+ "id": 2,
+ "type": "TXT",
+ "name": "bar",
+ "value": "bbb",
+ "ttl": 120,
+ "sourceId": 456
+ }
+ ]
+}
diff --git a/providers/dns/dode/internal/client_test.go b/providers/dns/dode/internal/client_test.go
index 139a0939a..6fbaa8c1d 100644
--- a/providers/dns/dode/internal/client_test.go
+++ b/providers/dns/dode/internal/client_test.go
@@ -1,91 +1,43 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- query := req.URL.Query()
- if query.Get("token") != "secret" {
- http.Error(rw, fmt.Sprintf("invalid credentials: %q", query.Get("token")), http.StatusUnauthorized)
- return
- }
-
- if query.Get("domain") != "example.com" {
- http.Error(rw, fmt.Sprintf("invalid domain: %q", query.Get("domain")), http.StatusBadRequest)
- return
- }
-
- if query.Has("action") {
- if query.Get("action") != "delete" {
- http.Error(rw, fmt.Sprintf("invalid action: %q", query.Get("action")), http.StatusBadRequest)
- return
- }
- } else {
- if query.Get("value") != "value" {
- http.Error(rw, fmt.Sprintf("invalid value: %q", query.Get("value")), http.StatusBadRequest)
- return
- }
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("secret")
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
- return client
+ return client, nil
}
func TestClient_UpdateTxtRecord(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/letsencrypt", http.StatusOK, "success.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /letsencrypt", servermock.ResponseFromFixture("success.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("domain", "example.com").
+ With("token", "secret").
+ With("value", "value")).
+ Build(t)
err := client.UpdateTxtRecord(t.Context(), "example.com.", "value", false)
require.NoError(t, err)
}
func TestClient_UpdateTxtRecord_clear(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/letsencrypt", http.StatusOK, "success.json")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /letsencrypt", servermock.ResponseFromFixture("success.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("action", "delete").
+ With("domain", "example.com").
+ With("token", "secret")).
+ Build(t)
err := client.UpdateTxtRecord(t.Context(), "example.com.", "value", true)
require.NoError(t, err)
diff --git a/providers/dns/domeneshop/internal/client_test.go b/providers/dns/domeneshop/internal/client_test.go
index 1f4265d03..beddc1cb2 100644
--- a/providers/dns/domeneshop/internal/client_test.go
+++ b/providers/dns/domeneshop/internal/client_test.go
@@ -1,121 +1,56 @@
package internal
import (
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-const authorizationHeader = "Authorization"
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("token", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("token", "secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("token", "secret"),
+ )
}
func TestClient_CreateTXTRecord(t *testing.T) {
- client, mux := setupTest(t)
+ client := mockBuilder().
+ Route("POST /domains/1/dns",
+ servermock.ResponseFromFixture("create_record.json"),
+ servermock.CheckRequestJSONBodyFromFile("create_record-request.json")).
+ Build(t)
- mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "Basic dG9rZW46c2VjcmV0" {
- http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized)
- return
- }
-
- _, _ = rw.Write([]byte(`{"id": 1}`))
- })
-
- err := client.CreateTXTRecord(t.Context(), &Domain{ID: 1}, "example", "txtTXTtxt")
+ err := client.CreateTXTRecord(t.Context(), &Domain{ID: 1}, "example.com", "txtTXTtxt")
require.NoError(t, err)
}
func TestClient_DeleteTXTRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "Basic dG9rZW46c2VjcmV0" {
- http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized)
- return
- }
-
- _, _ = rw.Write([]byte(`[
- {
- "id": 1,
- "host": "example.com",
- "ttl": 3600,
- "type": "TXT",
- "data": "txtTXTtxt"
- }
-]`))
- })
-
- mux.HandleFunc("/domains/1/dns/1", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "Basic dG9rZW46c2VjcmV0" {
- http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /domains/1/dns",
+ servermock.ResponseFromFixture("delete_record.json")).
+ Route("DELETE /domains/1/dns/1", nil).
+ Build(t)
err := client.DeleteTXTRecord(t.Context(), &Domain{ID: 1}, "example.com", "txtTXTtxt")
require.NoError(t, err)
}
func TestClient_getDNSRecordByHostData(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/1/dns", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "Basic dG9rZW46c2VjcmV0" {
- http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized)
- return
- }
-
- _, _ = rw.Write([]byte(`[
- {
- "id": 1,
- "host": "example.com",
- "ttl": 3600,
- "type": "TXT",
- "data": "txtTXTtxt"
- }
-]`))
- })
+ client := mockBuilder().
+ Route("GET /domains/1/dns",
+ servermock.ResponseFromFixture("getDnsRecords.json")).
+ Build(t)
record, err := client.getDNSRecordByHostData(t.Context(), Domain{ID: 1}, "example.com", "txtTXTtxt")
require.NoError(t, err)
@@ -132,43 +67,10 @@ func TestClient_getDNSRecordByHostData(t *testing.T) {
}
func TestClient_GetDomainByName(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "Basic dG9rZW46c2VjcmV0" {
- http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized)
- return
- }
-
- _, _ = rw.Write([]byte(`[
- {
- "id": 1,
- "domain": "example.com",
- "expiry_date": "2019-08-24",
- "registered_date": "2019-08-24",
- "renew": true,
- "registrant": "Ola Nordmann",
- "status": "active",
- "nameservers": [
- "ns1.hyp.net",
- "ns2.hyp.net",
- "ns3.hyp.net"
- ],
- "services": {
- "registrar": true,
- "dns": true,
- "email": true,
- "webhotel": "none"
- }
- }
-]`))
- })
+ client := mockBuilder().
+ Route("GET /domains/",
+ servermock.ResponseFromFixture("getDomains.json")).
+ Build(t)
domain, err := client.GetDomainByName(t.Context(), "example.com")
require.NoError(t, err)
diff --git a/providers/dns/domeneshop/internal/fixtures/create_record-request.json b/providers/dns/domeneshop/internal/fixtures/create_record-request.json
new file mode 100644
index 000000000..6bd3ca4ce
--- /dev/null
+++ b/providers/dns/domeneshop/internal/fixtures/create_record-request.json
@@ -0,0 +1,7 @@
+{
+ "data": "txtTXTtxt",
+ "host": "example.com",
+ "id": 0,
+ "ttl": 300,
+ "type": "TXT"
+}
diff --git a/providers/dns/domeneshop/internal/fixtures/create_record.json b/providers/dns/domeneshop/internal/fixtures/create_record.json
new file mode 100644
index 000000000..2572ae5fe
--- /dev/null
+++ b/providers/dns/domeneshop/internal/fixtures/create_record.json
@@ -0,0 +1,3 @@
+{
+ "id": 1
+}
diff --git a/providers/dns/domeneshop/internal/fixtures/delete_record.json b/providers/dns/domeneshop/internal/fixtures/delete_record.json
new file mode 100644
index 000000000..f3f987eef
--- /dev/null
+++ b/providers/dns/domeneshop/internal/fixtures/delete_record.json
@@ -0,0 +1,9 @@
+[
+ {
+ "id": 1,
+ "host": "example.com",
+ "ttl": 3600,
+ "type": "TXT",
+ "data": "txtTXTtxt"
+ }
+]
diff --git a/providers/dns/domeneshop/internal/fixtures/getDnsRecords.json b/providers/dns/domeneshop/internal/fixtures/getDnsRecords.json
new file mode 100644
index 000000000..f3f987eef
--- /dev/null
+++ b/providers/dns/domeneshop/internal/fixtures/getDnsRecords.json
@@ -0,0 +1,9 @@
+[
+ {
+ "id": 1,
+ "host": "example.com",
+ "ttl": 3600,
+ "type": "TXT",
+ "data": "txtTXTtxt"
+ }
+]
diff --git a/providers/dns/domeneshop/internal/fixtures/getDomains.json b/providers/dns/domeneshop/internal/fixtures/getDomains.json
new file mode 100644
index 000000000..b491d7f53
--- /dev/null
+++ b/providers/dns/domeneshop/internal/fixtures/getDomains.json
@@ -0,0 +1,22 @@
+[
+ {
+ "id": 1,
+ "domain": "example.com",
+ "expiry_date": "2019-08-24",
+ "registered_date": "2019-08-24",
+ "renew": true,
+ "registrant": "Ola Nordmann",
+ "status": "active",
+ "nameservers": [
+ "ns1.hyp.net",
+ "ns2.hyp.net",
+ "ns3.hyp.net"
+ ],
+ "services": {
+ "registrar": true,
+ "dns": true,
+ "email": true,
+ "webhotel": "none"
+ }
+ }
+]
diff --git a/providers/dns/dreamhost/dreamhost_test.go b/providers/dns/dreamhost/dreamhost_test.go
index 0f91ffae2..f85e00da4 100644
--- a/providers/dns/dreamhost/dreamhost_test.go
+++ b/providers/dns/dreamhost/dreamhost_test.go
@@ -1,13 +1,12 @@
package dreamhost
import (
- "fmt"
- "net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -23,22 +22,15 @@ const (
fakeKeyAuth = "w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"
)
-func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.APIKey = fakeAPIKey
+ config.BaseURL = server.URL
+ config.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- config := NewDefaultConfig()
- config.APIKey = fakeAPIKey
- config.BaseURL = server.URL
- config.HTTPClient = server.Client()
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- return provider, mux
+ return NewDNSProviderConfig(config)
+ })
}
func TestNewDNSProvider(t *testing.T) {
@@ -115,67 +107,48 @@ func TestNewDNSProviderConfig(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
-
- q := r.URL.Query()
- assert.Equal(t, fakeAPIKey, q.Get("key"))
- assert.Equal(t, "dns-add_record", q.Get("cmd"))
- assert.Equal(t, "json", q.Get("format"))
- assert.Equal(t, "_acme-challenge.example.com", q.Get("record"))
- assert.Equal(t, fakeKeyAuth, q.Get("value"))
- assert.Equal(t, "Managed+By+lego", q.Get("comment"))
-
- _, err := fmt.Fprintf(w, `{"data":"record_added","result":"success"}`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ provider := mockBuilder().
+ Route("GET /",
+ servermock.RawStringResponse(`{"data":"record_added","result":"success"}`),
+ servermock.CheckQueryParameter().Strict().
+ With("cmd", "dns-add_record").
+ With("comment", "Managed+By+lego").
+ With("format", "json").
+ With("record", "_acme-challenge.example.com").
+ With("type", "TXT").
+ With("key", fakeAPIKey).
+ With("value", fakeKeyAuth),
+ ).
+ Build(t)
err := provider.Present("example.com", "", fakeChallengeToken)
require.NoError(t, err)
}
func TestDNSProvider_PresentFailed(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
-
- _, err := fmt.Fprintf(w, `{"data":"record_already_exists_remove_first","result":"error"}`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ provider := mockBuilder().
+ Route("GET /",
+ servermock.RawStringResponse(`{"data":"record_already_exists_remove_first","result":"error"}`)).
+ Build(t)
err := provider.Present("example.com", "", fakeChallengeToken)
require.EqualError(t, err, "dreamhost: add TXT record failed: record_already_exists_remove_first")
}
func TestDNSProvider_Cleanup(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
-
- q := r.URL.Query()
- assert.Equal(t, fakeAPIKey, q.Get("key"), "key mismatch")
- assert.Equal(t, "dns-remove_record", q.Get("cmd"), "cmd mismatch")
- assert.Equal(t, "json", q.Get("format"))
- assert.Equal(t, "_acme-challenge.example.com", q.Get("record"))
- assert.Equal(t, fakeKeyAuth, q.Get("value"), "value mismatch")
- assert.Equal(t, "Managed+By+lego", q.Get("comment"))
-
- _, err := fmt.Fprintf(w, `{"data":"record_removed","result":"success"}`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ provider := mockBuilder().
+ Route("GET /",
+ servermock.RawStringResponse(`{"data":"record_removed","result":"success"}`),
+ servermock.CheckQueryParameter().Strict().
+ With("cmd", "dns-remove_record").
+ With("comment", "Managed+By+lego").
+ With("format", "json").
+ With("record", "_acme-challenge.example.com").
+ With("type", "TXT").
+ With("key", fakeAPIKey).
+ With("value", fakeKeyAuth),
+ ).
+ Build(t)
err := provider.CleanUp("example.com", "", fakeChallengeToken)
require.NoError(t, err, "failed to remove TXT record")
diff --git a/providers/dns/dreamhost/internal/client_test.go b/providers/dns/dreamhost/internal/client_test.go
index eff520df0..a836658f9 100644
--- a/providers/dns/dreamhost/internal/client_test.go
+++ b/providers/dns/dreamhost/internal/client_test.go
@@ -1,15 +1,59 @@
package internal
import (
+ "net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-const fakeAPIKey = "asdf1234"
+func setupClient(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.BaseURL = server.URL
+ client.HTTPClient = server.Client()
+
+ return client, nil
+}
+
+func TestClient_AddRecord(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", servermock.RawStringResponse(`{}`),
+ servermock.CheckQueryParameter().Strict().
+ With("cmd", "dns-add_record").
+ With("comment", "Managed+By+lego").
+ With("format", "json").
+ With("key", "secret").
+ With("record", "example.com").
+ With("type", "TXT").
+ With("value", "aaa")).
+ Build(t)
+
+ err := client.AddRecord(t.Context(), "example.com", "aaa")
+ require.NoError(t, err)
+}
+
+func TestClient_RemoveRecord(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", servermock.RawStringResponse(`{}`),
+ servermock.CheckQueryParameter().Strict().
+ With("cmd", "dns-remove_record").
+ With("comment", "Managed+By+lego").
+ With("format", "json").
+ With("key", "secret").
+ With("record", "example.com").
+ With("type", "TXT").
+ With("value", "aaa")).
+ Build(t)
+
+ err := client.RemoveRecord(t.Context(), "example.com", "aaa")
+ require.NoError(t, err)
+}
func TestClient_buildQuery(t *testing.T) {
+ const fakeAPIKey = "asdf1234"
+
testCases := []struct {
desc string
apiKey string
diff --git a/providers/dns/duckdns/internal/client.go b/providers/dns/duckdns/internal/client.go
index 0ed1bc864..ae86f64c8 100644
--- a/providers/dns/duckdns/internal/client.go
+++ b/providers/dns/duckdns/internal/client.go
@@ -21,6 +21,7 @@ const defaultBaseURL = "https://www.duckdns.org/update"
type Client struct {
token string
+ baseURL string
HTTPClient *http.Client
}
@@ -28,6 +29,7 @@ type Client struct {
func NewClient(token string) *Client {
return &Client{
token: token,
+ baseURL: defaultBaseURL,
HTTPClient: &http.Client{Timeout: 5 * time.Second},
}
}
@@ -44,7 +46,7 @@ func (c Client) RemoveTXTRecord(ctx context.Context, domain string) error {
// To update the TXT record we just need to make one simple get request.
// In DuckDNS you only have one TXT record shared with the domain and all subdomains.
func (c Client) UpdateTxtRecord(ctx context.Context, domain, txt string, clearRecord bool) error {
- endpoint, _ := url.Parse(defaultBaseURL)
+ endpoint, _ := url.Parse(c.baseURL)
mainDomain := getMainDomain(domain)
if mainDomain == "" {
diff --git a/providers/dns/duckdns/internal/client_test.go b/providers/dns/duckdns/internal/client_test.go
index 4df17d049..aaa441fad 100644
--- a/providers/dns/duckdns/internal/client_test.go
+++ b/providers/dns/duckdns/internal/client_test.go
@@ -1,11 +1,50 @@
package internal
import (
+ "net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
+func setupClient(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.baseURL = server.URL
+ client.HTTPClient = server.Client()
+
+ return client, nil
+}
+
+func TestClient_AddTXTRecord(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", servermock.RawStringResponse("OK"),
+ servermock.CheckQueryParameter().Strict().
+ With("clear", "false").
+ With("domains", "com").
+ With("token", "secret").
+ With("txt", "value")).
+ Build(t)
+
+ err := client.AddTXTRecord(t.Context(), "example.com", "value")
+ require.NoError(t, err)
+}
+
+func TestClient_RemoveTXTRecord(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", servermock.RawStringResponse("OK"),
+ servermock.CheckQueryParameter().Strict().
+ With("clear", "true").
+ With("domains", "com").
+ With("token", "secret").
+ With("txt", "")).
+ Build(t)
+
+ err := client.RemoveTXTRecord(t.Context(), "example.com")
+ require.NoError(t, err)
+}
+
func Test_getMainDomain(t *testing.T) {
testCases := []struct {
desc string
diff --git a/providers/dns/dyn/internal/client_test.go b/providers/dns/dyn/internal/client_test.go
index c6cdff9d5..f166e7d8d 100644
--- a/providers/dns/dyn/internal/client_test.go
+++ b/providers/dns/dyn/internal/client_test.go
@@ -1,120 +1,58 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handlerFunc http.HandlerFunc) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, handlerFunc)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("bob", "user", "secret")
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
- return client
+ return client, nil
}
-func authenticatedHandler(method string, status int, file string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("bob", "user", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- token := req.Header.Get(authTokenHeader)
- if token != "tok" {
- http.Error(rw, fmt.Sprintf("invalid credentials: %q", token), http.StatusUnauthorized)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
-
-func unauthenticatedHandler(method string, status int, file string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- token := req.Header.Get(authTokenHeader)
- if token != "" {
- http.Error(rw, fmt.Sprintf("invalid credentials: %q", token), http.StatusUnauthorized)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders())
}
func TestClient_Publish(t *testing.T) {
- client := setupTest(t, "/Zone/example.com", unauthenticatedHandler(http.MethodPut, http.StatusOK, "publish.json"))
+ client := mockBuilder().
+ Route("PUT /Zone/example.com", servermock.ResponseFromFixture("publish.json"),
+ servermock.CheckRequestJSONBody(`{"publish":true,"notes":"my message"}`)).
+ Build(t)
err := client.Publish(t.Context(), "example.com", "my message")
require.NoError(t, err)
}
func TestClient_AddTXTRecord(t *testing.T) {
- client := setupTest(t, "/TXTRecord/example.com/example.com.", unauthenticatedHandler(http.MethodPost, http.StatusCreated, "create-txt-record.json"))
+ client := mockBuilder().
+ Route("POST /TXTRecord/example.com/example.com.", servermock.ResponseFromFixture("create-txt-record.json"),
+ servermock.CheckRequestJSONBody(`{"rdata":{"txtdata":"txt"},"ttl":"120"}`)).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "example.com", "example.com.", "txt", 120)
require.NoError(t, err)
}
func TestClient_RemoveTXTRecord(t *testing.T) {
- client := setupTest(t, "/TXTRecord/example.com/example.com.", unauthenticatedHandler(http.MethodDelete, http.StatusOK, ""))
+ client := mockBuilder().
+ Route("DELETE /TXTRecord/example.com/example.com.", nil).
+ Build(t)
err := client.RemoveTXTRecord(t.Context(), "example.com", "example.com.")
require.NoError(t, err)
diff --git a/providers/dns/dyn/internal/session_test.go b/providers/dns/dyn/internal/session_test.go
index 5a939f40c..349b1b190 100644
--- a/providers/dns/dyn/internal/session_test.go
+++ b/providers/dns/dyn/internal/session_test.go
@@ -2,9 +2,9 @@ package internal
import (
"context"
- "net/http"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -16,7 +16,10 @@ func mockContext(t *testing.T) context.Context {
}
func TestClient_login(t *testing.T) {
- client := setupTest(t, "/Session", unauthenticatedHandler(http.MethodPost, http.StatusOK, "login.json"))
+ client := mockBuilder().
+ Route("POST /Session", servermock.ResponseFromFixture("login.json"),
+ servermock.CheckRequestJSONBody(`{"customer_name":"bob","user_name":"user","password":"secret"}`)).
+ Build(t)
sess, err := client.login(t.Context())
require.NoError(t, err)
@@ -27,14 +30,22 @@ func TestClient_login(t *testing.T) {
}
func TestClient_Logout(t *testing.T) {
- client := setupTest(t, "/Session", authenticatedHandler(http.MethodDelete, http.StatusOK, ""))
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ With(authTokenHeader, "tok"),
+ ).
+ Route("DELETE /Session", nil).
+ Build(t)
err := client.Logout(mockContext(t))
require.NoError(t, err)
}
func TestClient_CreateAuthenticatedContext(t *testing.T) {
- client := setupTest(t, "/Session", unauthenticatedHandler(http.MethodPost, http.StatusOK, "login.json"))
+ client := mockBuilder().
+ Route("POST /Session", servermock.ResponseFromFixture("login.json"),
+ servermock.CheckRequestJSONBody(`{"customer_name":"bob","user_name":"user","password":"secret"}`)).
+ Build(t)
ctx, err := client.CreateAuthenticatedContext(t.Context())
require.NoError(t, err)
diff --git a/providers/dns/dyndnsfree/internal/client_test.go b/providers/dns/dyndnsfree/internal/client_test.go
index 206022d5c..d6f1d276b 100644
--- a/providers/dns/dyndnsfree/internal/client_test.go
+++ b/providers/dns/dyndnsfree/internal/client_test.go
@@ -1,56 +1,44 @@
package internal
import (
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, message string) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client, err := NewClient("user", "secret")
- require.NoError(t, err)
+ if err != nil {
+ return nil, err
+ }
- client.HTTPClient = server.Client()
client.baseURL = server.URL
+ client.HTTPClient = server.Client()
- mux.HandleFunc("GET /", func(rw http.ResponseWriter, req *http.Request) {
- query := req.URL.Query()
-
- username := query.Get("username")
- if username != "user" {
- http.Error(rw, "invalid username: "+username, http.StatusUnauthorized)
- return
- }
-
- password := query.Get("password")
- if password != "secret" {
- http.Error(rw, "invalid password: "+password, http.StatusUnauthorized)
- return
- }
-
- _, _ = rw.Write([]byte(message))
- })
-
- return client
+ return client, nil
}
func TestAddTXTRecord(t *testing.T) {
- client := setupTest(t, "success")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", servermock.RawStringResponse("success"),
+ servermock.CheckQueryParameter().Strict().
+ With("add_hostname", "sub.example.com").
+ With("hostname", "example.com").
+ With("password", "secret").
+ With("txt", "value").
+ With("username", "user")).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "example.com", "sub.example.com", "value")
require.NoError(t, err)
}
func TestAddTXTRecord_error(t *testing.T) {
- client := setupTest(t, "error: authentification failed")
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", servermock.RawStringResponse("error: authentification failed")).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "example.com", "sub.example.com", "value")
require.EqualError(t, err, "error: authentification failed")
diff --git a/providers/dns/dynu/internal/client_test.go b/providers/dns/dynu/internal/client_test.go
index 4f3a16be9..7dc94eca2 100644
--- a/providers/dns/dynu/internal/client_test.go
+++ b/providers/dns/dynu/internal/client_test.go
@@ -1,52 +1,27 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient()
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- open, err := os.Open(file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client := NewClient()
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestGetRootDomain(t *testing.T) {
@@ -64,9 +39,9 @@ func TestGetRootDomain(t *testing.T) {
}{
{
desc: "success",
- pattern: "/dns/getroot/test.lego.freeddns.org",
+ pattern: "GET /dns/getroot/test.lego.freeddns.org",
status: http.StatusOK,
- file: "./fixtures/get_root_domain.json",
+ file: "get_root_domain.json",
expected: expected{
domain: &DNSHostname{
APIException: &APIException{
@@ -81,9 +56,9 @@ func TestGetRootDomain(t *testing.T) {
},
{
desc: "invalid",
- pattern: "/dns/getroot/test.lego.freeddns.org",
+ pattern: "GET /dns/getroot/test.lego.freeddns.org",
status: http.StatusNotImplemented,
- file: "./fixtures/get_root_domain_invalid.json",
+ file: "get_root_domain_invalid.json",
expected: expected{
error: "API error: 501: Argument Exception: Invalid.",
},
@@ -94,7 +69,9 @@ func TestGetRootDomain(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client := setupTest(t, http.MethodGet, test.pattern, test.status, test.file)
+ client := mockBuilder().
+ Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status)).
+ Build(t)
domain, err := client.GetRootDomain(t.Context(), "test.lego.freeddns.org")
@@ -126,9 +103,9 @@ func TestGetRecords(t *testing.T) {
}{
{
desc: "success",
- pattern: "/dns/record/_acme-challenge.lego.freeddns.org",
+ pattern: "GET /dns/record/_acme-challenge.lego.freeddns.org",
status: http.StatusOK,
- file: "./fixtures/get_records.json",
+ file: "get_records.json",
expected: expected{
records: []DNSRecord{
{
@@ -160,18 +137,18 @@ func TestGetRecords(t *testing.T) {
},
{
desc: "empty",
- pattern: "/dns/record/_acme-challenge.lego.freeddns.org",
+ pattern: "GET /dns/record/_acme-challenge.lego.freeddns.org",
status: http.StatusOK,
- file: "./fixtures/get_records_empty.json",
+ file: "get_records_empty.json",
expected: expected{
records: []DNSRecord{},
},
},
{
desc: "invalid",
- pattern: "/dns/record/_acme-challenge.lego.freeddns.org",
+ pattern: "GET /dns/record/_acme-challenge.lego.freeddns.org",
status: http.StatusNotImplemented,
- file: "./fixtures/get_records_invalid.json",
+ file: "get_records_invalid.json",
expected: expected{
error: "API error: 501: Argument Exception: Invalid.",
},
@@ -182,7 +159,11 @@ func TestGetRecords(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client := setupTest(t, http.MethodGet, test.pattern, test.status, test.file)
+ client := mockBuilder().
+ Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status),
+ servermock.CheckQueryParameter().Strict().
+ With("recordType", "TXT")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "_acme-challenge.lego.freeddns.org", "TXT")
@@ -213,15 +194,15 @@ func TestAddNewRecord(t *testing.T) {
}{
{
desc: "success",
- pattern: "/dns/9007481/record",
+ pattern: "POST /dns/9007481/record",
status: http.StatusOK,
- file: "./fixtures/add_new_record.json",
+ file: "add_new_record.json",
},
{
desc: "invalid",
- pattern: "/dns/9007481/record",
+ pattern: "POST /dns/9007481/record",
status: http.StatusNotImplemented,
- file: "./fixtures/add_new_record_invalid.json",
+ file: "add_new_record_invalid.json",
expected: expected{
error: "API error: 501: Argument Exception: Invalid.",
},
@@ -232,7 +213,10 @@ func TestAddNewRecord(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client := setupTest(t, http.MethodPost, test.pattern, test.status, test.file)
+ client := mockBuilder().
+ Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status),
+ servermock.CheckRequestJSONBodyFromFile("add_new_record-request.json")).
+ Build(t)
record := DNSRecord{
Type: "TXT",
@@ -270,15 +254,15 @@ func TestDeleteRecord(t *testing.T) {
}{
{
desc: "success",
- pattern: "/",
+ pattern: "DELETE /",
status: http.StatusOK,
- file: "./fixtures/delete_record.json",
+ file: "delete_record.json",
},
{
desc: "invalid",
- pattern: "/",
+ pattern: "DELETE /",
status: http.StatusNotImplemented,
- file: "./fixtures/delete_record_invalid.json",
+ file: "delete_record_invalid.json",
expected: expected{
error: "API error: 501: Argument Exception: Invalid.",
},
@@ -289,7 +273,9 @@ func TestDeleteRecord(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client := setupTest(t, http.MethodDelete, test.pattern, test.status, test.file)
+ client := mockBuilder().
+ Route(test.pattern, servermock.ResponseFromFixture(test.file).WithStatusCode(test.status)).
+ Build(t)
err := client.DeleteRecord(t.Context(), 9007481, 6041418)
diff --git a/providers/dns/dynu/internal/fixtures/add_new_record-request.json b/providers/dns/dynu/internal/fixtures/add_new_record-request.json
new file mode 100644
index 000000000..f3c75ca36
--- /dev/null
+++ b/providers/dns/dynu/internal/fixtures/add_new_record-request.json
@@ -0,0 +1,9 @@
+{
+ "recordType": "TXT",
+ "domainName": "lego.freeddns.org",
+ "nodeName": "_acme-challenge",
+ "hostname": "_acme-challenge.lego.freeddns.org",
+ "state": true,
+ "textData": "txt_txt_txt_txt_txt_txt_txt_2",
+ "ttl": 300
+}
diff --git a/providers/dns/easydns/easydns_test.go b/providers/dns/easydns/easydns_test.go
index 972ff8cda..9a11ef6cc 100644
--- a/providers/dns/easydns/easydns_test.go
+++ b/providers/dns/easydns/easydns_test.go
@@ -2,7 +2,6 @@ package easydns
import (
"fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
@@ -10,12 +9,10 @@ import (
"time"
"github.com/go-acme/lego/v4/platform/tester"
- "github.com/stretchr/testify/assert"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-const authorizationHeader = "Authorization"
-
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(
@@ -24,26 +21,27 @@ var envTest = tester.NewEnvTest(
EnvKey).
WithDomain(envDomain)
-func setupTest(t *testing.T) (*DNSProvider, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ endpoint, err := url.Parse(server.URL)
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ config := NewDefaultConfig()
+ config.Token = "TOKEN"
+ config.Key = "SECRET"
+ config.Endpoint = endpoint
+ config.HTTPClient = server.Client()
- endpoint, err := url.Parse(server.URL)
- require.NoError(t, err)
-
- config := NewDefaultConfig()
- config.Token = "TOKEN"
- config.Key = "SECRET"
- config.Endpoint = endpoint
- config.HTTPClient = server.Client()
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- return provider, mux
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().
+ WithJSONHeaders().
+ WithAuthorization("Basic VE9LRU46U0VDUkVU"),
+ servermock.CheckQueryParameter().Strict().
+ With("format", "json"))
}
func TestNewDNSProvider(t *testing.T) {
@@ -145,78 +143,50 @@ func TestNewDNSProviderConfig(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
- assert.Equal(t, "format=json", r.URL.RawQuery, "query")
- assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader)
-
- w.WriteHeader(http.StatusOK)
- _, err := fmt.Fprintf(w, `{
- "msg": "string",
- "status": 200,
- "tm": 0,
- "data": [{
- "id": "60898922",
- "domain": "example.com",
- "host": "hosta",
- "ttl": "300",
- "prio": "0",
- "geozone_id": "0",
- "type": "A",
- "rdata": "1.2.3.4",
- "last_mod": "2019-08-28 19:09:50"
- }],
- "count": 0,
- "total": 0,
- "start": 0,
- "max": 0
-}
-`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
+ provider := mockBuilder().
+ Route("GET /zones/records/all/example.com",
+ servermock.RawStringResponse(`{
+ "msg": "string",
+ "status": 200,
+ "tm": 0,
+ "data": [{
+ "id": "60898922",
+ "domain": "example.com",
+ "host": "hosta",
+ "ttl": "300",
+ "prio": "0",
+ "geozone_id": "0",
+ "type": "A",
+ "rdata": "1.2.3.4",
+ "last_mod": "2019-08-28 19:09:50"
+ }],
+ "count": 0,
+ "total": 0,
+ "start": 0,
+ "max": 0
}
- })
-
- mux.HandleFunc("/zones/records/add/example.com/TXT", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodPut, r.Method, "method")
- assert.Equal(t, "format=json", r.URL.RawQuery, "query")
- assert.Equal(t, "application/json", r.Header.Get("Content-Type"), "Content-Type")
- assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader)
-
- reqBody, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- expectedReqBody := `{"domain":"example.com","host":"_acme-challenge","ttl":"120","prio":"0","type":"TXT","rdata":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"}
-`
- assert.Equal(t, expectedReqBody, string(reqBody))
-
- w.WriteHeader(http.StatusCreated)
- _, err = fmt.Fprintf(w, `{
- "msg": "OK",
- "tm": 1554681934,
- "data": {
- "host": "_acme-challenge",
- "geozone_id": 0,
- "ttl": "120",
- "prio": "0",
- "rdata": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM",
- "revoked": 0,
- "id": "123456789",
- "new_host": "_acme-challenge.example.com"
- },
- "status": 201
- }`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ `),
+ servermock.CheckQueryParameter().Strict().
+ With("format", "json")).
+ Route("PUT /zones/records/add/example.com/TXT",
+ servermock.RawStringResponse(`{
+ "msg": "OK",
+ "tm": 1554681934,
+ "data": {
+ "host": "_acme-challenge",
+ "geozone_id": 0,
+ "ttl": "120",
+ "prio": "0",
+ "rdata": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM",
+ "revoked": 0,
+ "id": "123456789",
+ "new_host": "_acme-challenge.example.com"
+ },
+ "status": 201
+ }`),
+ servermock.CheckRequestJSONBody(`{"domain":"example.com","host":"_acme-challenge","ttl":"120","prio":"0","type":"TXT","rdata":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"}
+`)).
+ Build(t)
err := provider.Present("example.com", "token", "keyAuth")
require.NoError(t, err)
@@ -224,163 +194,116 @@ func TestDNSProvider_Present(t *testing.T) {
}
func TestDNSProvider_Cleanup_WhenRecordIdNotSet_NoOp(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
- assert.Equal(t, "format=json", r.URL.RawQuery, "query")
- assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader)
-
- w.WriteHeader(http.StatusOK)
- _, err := fmt.Fprintf(w, `{
- "msg": "string",
- "status": 200,
- "tm": 0,
- "data": [{
- "id": "60898922",
- "domain": "example.com",
- "host": "hosta",
- "ttl": "300",
- "prio": "0",
- "geozone_id": "0",
- "type": "A",
- "rdata": "1.2.3.4",
- "last_mod": "2019-08-28 19:09:50"
- }],
- "count": 0,
- "total": 0,
- "start": 0,
- "max": 0
-}
-`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ provider := mockBuilder().
+ Route("GET /zones/records/all/_acme-challenge.example.com",
+ servermock.RawStringResponse(`{
+ "msg": "string",
+ "status": 200,
+ "tm": 0,
+ "data": [{
+ "id": "60898922",
+ "domain": "example.com",
+ "host": "hosta",
+ "ttl": "300",
+ "prio": "0",
+ "geozone_id": "0",
+ "type": "A",
+ "rdata": "1.2.3.4",
+ "last_mod": "2019-08-28 19:09:50"
+ }],
+ "count": 0,
+ "total": 0,
+ "start": 0,
+ "max": 0
+ }
+ `)).
+ Build(t)
err := provider.CleanUp("example.com", "token", "keyAuth")
require.NoError(t, err)
}
func TestDNSProvider_Cleanup_WhenRecordIdSet_DeletesTxtRecord(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
- assert.Equal(t, "format=json", r.URL.RawQuery, "query")
- assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader)
-
- w.WriteHeader(http.StatusOK)
- _, err := fmt.Fprintf(w, `{
- "msg": "string",
- "status": 200,
- "tm": 0,
- "data": [{
- "id": "60898922",
- "domain": "example.com",
- "host": "hosta",
- "ttl": "300",
- "prio": "0",
- "geozone_id": "0",
- "type": "A",
- "rdata": "1.2.3.4",
- "last_mod": "2019-08-28 19:09:50"
- }],
- "count": 0,
- "total": 0,
- "start": 0,
- "max": 0
-}
-`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodDelete, r.Method, "method")
- assert.Equal(t, "format=json", r.URL.RawQuery, "query")
- assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader)
-
- w.WriteHeader(http.StatusOK)
- _, err := fmt.Fprintf(w, `{
- "msg": "OK",
- "data": {
- "domain": "example.com",
- "id": "123456"
- },
- "status": 200
- }`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ provider := mockBuilder().
+ Route("GET /zones/records/all/_acme-challenge.example.com",
+ servermock.RawStringResponse(`{
+ "msg": "string",
+ "status": 200,
+ "tm": 0,
+ "data": [{
+ "id": "60898922",
+ "domain": "example.com",
+ "host": "hosta",
+ "ttl": "300",
+ "prio": "0",
+ "geozone_id": "0",
+ "type": "A",
+ "rdata": "1.2.3.4",
+ "last_mod": "2019-08-28 19:09:50"
+ }],
+ "count": 0,
+ "total": 0,
+ "start": 0,
+ "max": 0
+ }
+ `)).
+ Route("DELETE /zones/records/_acme-challenge.example.com/123456",
+ servermock.RawStringResponse(`{
+ "msg": "OK",
+ "data": {
+ "domain": "example.com",
+ "id": "123456"
+ },
+ "status": 200
+ }`)).
+ Build(t)
provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456"
+
err := provider.CleanUp("example.com", "token", "keyAuth")
require.NoError(t, err)
}
func TestDNSProvider_Cleanup_WhenHttpError_ReturnsError(t *testing.T) {
- provider, mux := setupTest(t)
-
- mux.HandleFunc("/zones/records/all/example.com", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodGet, r.Method, "method")
- assert.Equal(t, "format=json", r.URL.RawQuery, "query")
- assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader)
-
- w.WriteHeader(http.StatusOK)
- _, err := fmt.Fprintf(w, `{
- "msg": "string",
- "status": 200,
- "tm": 0,
- "data": [{
- "id": "60898922",
- "domain": "example.com",
- "host": "hosta",
- "ttl": "300",
- "prio": "0",
- "geozone_id": "0",
- "type": "A",
- "rdata": "1.2.3.4",
- "last_mod": "2019-08-28 19:09:50"
- }],
- "count": 0,
- "total": 0,
- "start": 0,
- "max": 0
-}
-`)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
errorMessage := `{
"error": {
"code": 406,
"message": "Provided id is invalid or you do not have permission to access it."
}
}`
- mux.HandleFunc("/zones/records/example.com/123456", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(t, http.MethodDelete, r.Method, "method")
- assert.Equal(t, "format=json", r.URL.RawQuery, "query")
- assert.Equal(t, "Basic VE9LRU46U0VDUkVU", r.Header.Get(authorizationHeader), authorizationHeader)
- w.WriteHeader(http.StatusNotAcceptable)
- _, err := fmt.Fprint(w, errorMessage)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ provider := mockBuilder().
+ Route("GET /zones/records/all/example.com",
+ servermock.RawStringResponse(`{
+ "msg": "string",
+ "status": 200,
+ "tm": 0,
+ "data": [{
+ "id": "60898922",
+ "domain": "example.com",
+ "host": "hosta",
+ "ttl": "300",
+ "prio": "0",
+ "geozone_id": "0",
+ "type": "A",
+ "rdata": "1.2.3.4",
+ "last_mod": "2019-08-28 19:09:50"
+ }],
+ "count": 0,
+ "total": 0,
+ "start": 0,
+ "max": 0
+}
+`)).
+ Route("DELETE /zones/records/example.com/123456",
+ servermock.RawStringResponse(errorMessage).
+ WithStatusCode(http.StatusNotAcceptable)).
+ Build(t)
provider.recordIDs["_acme-challenge.example.com.|pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM"] = "123456"
+
err := provider.CleanUp("example.com", "token", "keyAuth")
+
expectedError := fmt.Sprintf("easydns: unexpected status code: [status code: 406] body: %v", errorMessage)
require.EqualError(t, err, expectedError)
}
diff --git a/providers/dns/easydns/internal/client_test.go b/providers/dns/easydns/internal/client_test.go
index 02d46a5a7..bf4e1e45b 100644
--- a/providers/dns/easydns/internal/client_test.go
+++ b/providers/dns/easydns/internal/client_test.go
@@ -1,73 +1,34 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("tok", "k")
+ client.HTTPClient = server.Client()
+ client.BaseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- token, key, ok := req.BasicAuth()
- if token != "tok" || key != "k" || !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if req.URL.Query().Get("format") != "json" {
- http.Error(rw, fmt.Sprintf("invalid format: %s", req.URL.Query().Get("format")), http.StatusBadRequest)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client := NewClient("tok", "k")
- client.HTTPClient = server.Client()
- client.BaseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("tok", "k"),
+ )
}
func TestClient_ListZones(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/zones/records/all/example.com", http.StatusOK, "list-zone.json")
+ client := mockBuilder().
+ Route("GET /zones/records/all/example.com", servermock.ResponseFromFixture("list-zone.json")).
+ Build(t)
zones, err := client.ListZones(t.Context(), "example.com")
require.NoError(t, err)
@@ -87,14 +48,20 @@ func TestClient_ListZones(t *testing.T) {
}
func TestClient_ListZones_error(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/zones/records/all/example.com", http.StatusOK, "error1.json")
+ client := mockBuilder().
+ Route("GET /zones/records/all/example.com", servermock.ResponseFromFixture("error1.json")).
+ Build(t)
_, err := client.ListZones(t.Context(), "example.com")
require.EqualError(t, err, "code 420: Enhance Your Calm. Rate limit exceeded (too many requests) OR you did NOT provide any credentials with your request!")
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, http.MethodPut, "/zones/records/add/example.com/TXT", http.StatusCreated, "add-record.json")
+ client := mockBuilder().
+ Route("PUT /zones/records/add/example.com/TXT",
+ servermock.ResponseFromFixture("add-record.json").WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBody(`{"domain":"example.com","host":"test631","ttl":"300","prio":"0","type":"TXT","rdata":"txt"}`)).
+ Build(t)
record := ZoneRecord{
Domain: "example.com",
@@ -112,7 +79,10 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, http.MethodPut, "/zones/records/add/example.com/TXT", http.StatusCreated, "error1.json")
+ client := mockBuilder().
+ Route("PUT /zones/records/add/example.com/TXT",
+ servermock.ResponseFromFixture("error1.json").WithStatusCode(http.StatusCreated)).
+ Build(t)
record := ZoneRecord{
Domain: "example.com",
@@ -128,7 +98,9 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/zones/records/example.com/xxx", http.StatusOK, "")
+ client := mockBuilder().
+ Route("DELETE /zones/records/example.com/xxx", nil).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", "xxx")
require.NoError(t, err)
diff --git a/providers/dns/efficientip/internal/client_test.go b/providers/dns/efficientip/internal/client_test.go
index 137f2628c..5d68b7d7f 100644
--- a/providers/dns/efficientip/internal/client_test.go
+++ b/providers/dns/efficientip/internal/client_test.go
@@ -1,79 +1,38 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ srvURL, _ := url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client := NewClient(srvURL.Host, "user", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- username, password, ok := req.BasicAuth()
- if !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if username != "user" {
- http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized)
- return
- }
-
- if password != "secret" {
- http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- srvURL, _ := url.Parse(server.URL)
-
- client := NewClient(srvURL.Host, "user", "secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("user", "secret"),
+ )
}
func TestListRecords(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/dns_rr_list", http.StatusOK, "dns_rr_list.json")
+ client := mockBuilder().
+ Route("GET /dns_rr_list", servermock.ResponseFromFixture("dns_rr_list.json")).
+ Build(t)
- ctx := t.Context()
-
- records, err := client.ListRecords(ctx)
+ records, err := client.ListRecords(t.Context())
require.NoError(t, err)
expected := []ResourceRecord{
@@ -336,11 +295,13 @@ func TestListRecords(t *testing.T) {
}
func TestGetRecord(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/dns_rr_info", http.StatusOK, "dns_rr_info.json")
+ client := mockBuilder().
+ Route("GET /dns_rr_info", servermock.ResponseFromFixture("dns_rr_info.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("rr_id", "239")).
+ Build(t)
- ctx := t.Context()
-
- record, err := client.GetRecord(ctx, "239")
+ record, err := client.GetRecord(t.Context(), "239")
require.NoError(t, err)
expected := &ResourceRecord{
@@ -383,9 +344,11 @@ func TestGetRecord(t *testing.T) {
}
func TestAddRecord(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/dns_rr_add", http.StatusCreated, "dns_rr_add.json")
-
- ctx := t.Context()
+ client := mockBuilder().
+ Route("POST /dns_rr_add",
+ servermock.ResponseFromFixture("dns_rr_add.json").WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBody(`{"dns_name":"dns.smart","dnsview_name":"external","rr_name":"test.example.com","rr_type":"TXT","value1":"test"}`)).
+ Build(t)
r := ResourceRecord{
RRName: "test.example.com",
@@ -395,7 +358,7 @@ func TestAddRecord(t *testing.T) {
DNSViewName: "external",
}
- resp, err := client.AddRecord(ctx, r)
+ resp, err := client.AddRecord(t.Context(), r)
require.NoError(t, err)
expected := &BaseOutput{RetOID: "239"}
@@ -404,11 +367,13 @@ func TestAddRecord(t *testing.T) {
}
func TestDeleteRecord(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns_rr_delete", http.StatusOK, "dns_rr_delete.json")
+ client := mockBuilder().
+ Route("DELETE /dns_rr_delete", servermock.ResponseFromFixture("dns_rr_delete.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("rr_id", "251")).
+ Build(t)
- ctx := t.Context()
-
- resp, err := client.DeleteRecord(ctx, DeleteInputParameters{RRID: "251"})
+ resp, err := client.DeleteRecord(t.Context(), DeleteInputParameters{RRID: "251"})
require.NoError(t, err)
expected := &BaseOutput{RetOID: "251"}
@@ -417,10 +382,11 @@ func TestDeleteRecord(t *testing.T) {
}
func TestDeleteRecord_error(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns_rr_delete", http.StatusBadRequest, "dns_rr_delete-error.json")
+ client := mockBuilder().
+ Route("DELETE /dns_rr_delete",
+ servermock.ResponseFromFixture("dns_rr_delete-error.json").WithStatusCode(http.StatusBadRequest)).
+ Build(t)
- ctx := t.Context()
-
- _, err := client.DeleteRecord(ctx, DeleteInputParameters{RRID: "251"})
+ _, err := client.DeleteRecord(t.Context(), DeleteInputParameters{RRID: "251"})
require.ErrorAs(t, err, &APIError{})
}
diff --git a/providers/dns/epik/internal/client_test.go b/providers/dns/epik/internal/client_test.go
index b23862207..b7c6f97df 100644
--- a/providers/dns/epik/internal/client_test.go
+++ b/providers/dns/epik/internal/client_test.go
@@ -1,37 +1,36 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestClient_GetDNSRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodGet, http.StatusOK, "getDnsRecord.json"))
+ client := mockBuilder().
+ Route("GET /domains/example.com/records",
+ servermock.ResponseFromFixture("getDnsRecord.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("SIGNATURE", "secret")).
+ Build(t)
records, err := client.GetDNSRecords(t.Context(), "example.com")
require.NoError(t, err)
@@ -88,18 +87,25 @@ func TestClient_GetDNSRecords(t *testing.T) {
}
func TestClient_GetDNSRecords_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("GET /domains/example.com/records",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized),
+ servermock.CheckQueryParameter().Strict().
+ With("SIGNATURE", "secret")).
+ Build(t)
_, err := client.GetDNSRecords(t.Context(), "example.com")
require.Error(t, err)
}
func TestClient_CreateHostRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodPost, http.StatusOK, "createHostRecord.json"))
+ client := mockBuilder().
+ Route("POST /domains/example.com/records",
+ servermock.ResponseFromFixture("createHostRecord.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("SIGNATURE", "secret")).
+ Build(t)
record := RecordRequest{
Host: "www2",
@@ -121,9 +127,13 @@ func TestClient_CreateHostRecord(t *testing.T) {
}
func TestClient_CreateHostRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodPost, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("POST /domains/example.com/records",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized),
+ servermock.CheckQueryParameter().Strict().
+ With("SIGNATURE", "secret")).
+ Build(t)
record := RecordRequest{
Host: "www2",
@@ -138,9 +148,13 @@ func TestClient_CreateHostRecord_error(t *testing.T) {
}
func TestClient_RemoveHostRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodDelete, http.StatusOK, "removeHostRecord.json"))
+ client := mockBuilder().
+ Route("DELETE /domains/example.com/records",
+ servermock.ResponseFromFixture("removeHostRecord.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("ID", "abc123").
+ With("SIGNATURE", "secret")).
+ Build(t)
data, err := client.RemoveHostRecord(t.Context(), "example.com", "abc123")
require.NoError(t, err)
@@ -154,45 +168,12 @@ func TestClient_RemoveHostRecord(t *testing.T) {
}
func TestClient_RemoveHostRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/example.com/records", testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("DELETE /domains/example.com/records",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.RemoveHostRecord(t.Context(), "example.com", "abc123")
require.Error(t, err)
}
-
-func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.URL.Query().Get("SIGNATURE")
- if auth != "secret" {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(statusCode)
-
- if statusCode == http.StatusNoContent {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
- }
-}
diff --git a/providers/dns/f5xc/internal/client_test.go b/providers/dns/f5xc/internal/client_test.go
index 7b53a3bce..0357abb16 100644
--- a/providers/dns/f5xc/internal/client_test.go
+++ b/providers/dns/f5xc/internal/client_test.go
@@ -1,58 +1,39 @@
package internal
import (
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("secret", "shortname")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("secret", "shortname")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("APIToken secret"))
}
func TestClient_Create(t *testing.T) {
- client := setupTest(t, "POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", http.StatusOK, "create.json")
+ client := mockBuilder().
+ Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA",
+ servermock.ResponseFromFixture("create.json"),
+ servermock.CheckRequestJSONBody(`{"dns_zone_name":"example.com","group_name":"groupA","rrset":{"description":"lego","ttl":60,"txt_record":{"name":"wwww","values":["txt"]}}}`)).
+ Build(t)
rrSet := RRSet{
Description: "lego",
@@ -82,7 +63,10 @@ func TestClient_Create(t *testing.T) {
}
func TestClient_Create_error(t *testing.T) {
- client := setupTest(t, "POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA", http.StatusBadRequest, "")
+ client := mockBuilder().
+ Route("POST /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA",
+ servermock.Noop().WithStatusCode(http.StatusBadRequest)).
+ Build(t)
rrSet := RRSet{
Description: "lego",
@@ -98,7 +82,10 @@ func TestClient_Create_error(t *testing.T) {
}
func TestClient_Get(t *testing.T) {
- client := setupTest(t, "GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", http.StatusOK, "get.json")
+ client := mockBuilder().
+ Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT",
+ servermock.ResponseFromFixture("get.json")).
+ Build(t)
result, err := client.GetRRSet(t.Context(), "example.com", "groupA", "www", "TXT")
require.NoError(t, err)
@@ -122,7 +109,10 @@ func TestClient_Get(t *testing.T) {
}
func TestClient_Get_not_found(t *testing.T) {
- client := setupTest(t, "GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", http.StatusNotFound, "error_404.json")
+ client := mockBuilder().
+ Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT",
+ servermock.ResponseFromFixture("error_404.json").WithStatusCode(http.StatusNotFound)).
+ Build(t)
result, err := client.GetRRSet(t.Context(), "example.com", "groupA", "www", "TXT")
require.NoError(t, err)
@@ -131,14 +121,20 @@ func TestClient_Get_not_found(t *testing.T) {
}
func TestClient_Get_error(t *testing.T) {
- client := setupTest(t, "GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", http.StatusBadRequest, "")
+ client := mockBuilder().
+ Route("GET /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT",
+ servermock.Noop().WithStatusCode(http.StatusBadRequest)).
+ Build(t)
_, err := client.GetRRSet(t.Context(), "example.com", "groupA", "www", "TXT")
require.Error(t, err)
}
func TestClient_Delete(t *testing.T) {
- client := setupTest(t, "DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", http.StatusOK, "get.json")
+ client := mockBuilder().
+ Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT",
+ servermock.ResponseFromFixture("get.json")).
+ Build(t)
result, err := client.DeleteRRSet(t.Context(), "example.com", "groupA", "www", "TXT")
require.NoError(t, err)
@@ -162,14 +158,21 @@ func TestClient_Delete(t *testing.T) {
}
func TestClient_Delete_error(t *testing.T) {
- client := setupTest(t, "DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", http.StatusBadRequest, "")
+ client := mockBuilder().
+ Route("DELETE /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT",
+ servermock.Noop().WithStatusCode(http.StatusBadRequest)).
+ Build(t)
_, err := client.DeleteRRSet(t.Context(), "example.com", "groupA", "www", "TXT")
require.Error(t, err)
}
func TestClient_Replace(t *testing.T) {
- client := setupTest(t, "PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", http.StatusOK, "get.json")
+ client := mockBuilder().
+ Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT",
+ servermock.ResponseFromFixture("get.json"),
+ servermock.CheckRequestJSONBody(`{"dns_zone_name":"example.com","group_name":"groupA","type":"TXT","rrset":{"description":"lego","ttl":60,"txt_record":{"name":"wwww","values":["txt"]}}}`)).
+ Build(t)
rrSet := RRSet{
Description: "lego",
@@ -202,7 +205,10 @@ func TestClient_Replace(t *testing.T) {
}
func TestClient_Replace_error(t *testing.T) {
- client := setupTest(t, "PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT", http.StatusBadRequest, "")
+ client := mockBuilder().
+ Route("PUT /api/config/dns/namespaces/system/dns_zones/example.com/rrsets/groupA/www/TXT",
+ servermock.Noop().WithStatusCode(http.StatusBadRequest)).
+ Build(t)
rrSet := RRSet{
Description: "lego",
diff --git a/providers/dns/gandi/gandi_test.go b/providers/dns/gandi/gandi_test.go
index 36bc4ccd2..4c37fb00e 100644
--- a/providers/dns/gandi/gandi_test.go
+++ b/providers/dns/gandi/gandi_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
@@ -119,38 +120,41 @@ func TestDNSProvider(t *testing.T) {
cleanupDeleteZoneRequestMock: cleanupDeleteZoneResponseMock,
}
- fakeKeyAuth := "XXXX"
-
regexpDate := regexp.MustCompile(`\[ACME Challenge [^\]:]*:[^\]]*\]`)
- // start fake RPC server
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, "text/xml", r.Header.Get("Content-Type"), "invalid content type")
+ provider := servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.BaseURL = server.URL + "/"
+ config.APIKey = "123412341234123412341234"
- req, errS := io.ReadAll(r.Body)
- require.NoError(t, errS)
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().WithContentType("text/xml"),
+ ).
+ Route("POST /", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ require.Equal(t, "text/xml", req.Header.Get("Content-Type"), "invalid content type")
- req = regexpDate.ReplaceAllLiteral(req, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`))
- resp, ok := serverResponses[string(req)]
- require.Truef(t, ok, "Server response for request not found: %s", string(req))
+ body, errS := io.ReadAll(req.Body)
+ require.NoError(t, errS)
- _, errS = io.Copy(w, strings.NewReader(resp))
- require.NoError(t, errS)
- }))
- t.Cleanup(server.Close)
+ body = regexpDate.ReplaceAllLiteral(body, []byte(`[ACME Challenge 01 Jan 16 00:00 +0000]`))
+ resp, ok := serverResponses[string(body)]
+ require.Truef(t, ok, "Server response for request not found: %s", string(body))
+
+ _, errS = io.Copy(rw, strings.NewReader(resp))
+ require.NoError(t, errS)
+ })).
+ Route("/", servermock.DumpRequest()).
+ Build(t)
+
+ fakeKeyAuth := "XXXX"
// define function to override findZoneByFqdn with
fakeFindZoneByFqdn := func(fqdn string) (string, error) {
return "example.com.", nil
}
- config := NewDefaultConfig()
- config.BaseURL = server.URL + "/"
- config.APIKey = "123412341234123412341234"
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
// override findZoneByFqdn function
savedFindZoneByFqdn := provider.findZoneByFqdn
t.Cleanup(func() {
@@ -159,7 +163,7 @@ func TestDNSProvider(t *testing.T) {
provider.findZoneByFqdn = fakeFindZoneByFqdn
// run Present
- err = provider.Present("abc.def.example.com", "", fakeKeyAuth)
+ err := provider.Present("abc.def.example.com", "", fakeKeyAuth)
require.NoError(t, err)
// run CleanUp
diff --git a/providers/dns/gandi/internal/client_test.go b/providers/dns/gandi/internal/client_test.go
new file mode 100644
index 000000000..573f812fa
--- /dev/null
+++ b/providers/dns/gandi/internal/client_test.go
@@ -0,0 +1,99 @@
+package internal
+
+import (
+ "net/http/httptest"
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.BaseURL = server.URL
+ client.HTTPClient = server.Client()
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithContentType("text/xml"),
+ )
+}
+
+func TestClient_GetZoneID(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("get_zone_id.xml"),
+ servermock.CheckRequestBodyFromFile("get_zone_id-request.xml").IgnoreWhitespace()).
+ Build(t)
+
+ zoneID, err := client.GetZoneID(t.Context(), "example.com")
+ require.NoError(t, err)
+
+ assert.Equal(t, 1, zoneID)
+}
+
+func TestClient_CloneZone(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("clone_zone.xml"),
+ servermock.CheckRequestBodyFromFile("clone_zone-request.xml").IgnoreWhitespace()).
+ Build(t)
+
+ zoneID, err := client.CloneZone(t.Context(), 6, "foo")
+ require.NoError(t, err)
+
+ assert.Equal(t, 1, zoneID)
+}
+
+func TestClient_NewZoneVersion(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("new_zone_version.xml"),
+ servermock.CheckRequestBodyFromFile("new_zone_version-request.xml").IgnoreWhitespace()).
+ Build(t)
+
+ zoneID, err := client.NewZoneVersion(t.Context(), 6)
+ require.NoError(t, err)
+
+ assert.Equal(t, 1, zoneID)
+}
+
+func TestClient_AddTXTRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("empty.xml"),
+ servermock.CheckRequestBodyFromFile("add_txt_record-request.xml").IgnoreWhitespace()).
+ Build(t)
+
+ err := client.AddTXTRecord(t.Context(), 1, 123, "foo", "content", 120)
+ require.NoError(t, err)
+}
+
+func TestClient_SetZoneVersion(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("set_zone_version.xml"),
+ servermock.CheckRequestBodyFromFile("set_zone_version-request.xml").IgnoreWhitespace()).
+ Build(t)
+
+ err := client.SetZoneVersion(t.Context(), 1, 123)
+ require.NoError(t, err)
+}
+
+func TestClient_SetZone(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("set_zone.xml"),
+ servermock.CheckRequestBodyFromFile("set_zone-request.xml").IgnoreWhitespace()).
+ Build(t)
+
+ err := client.SetZone(t.Context(), "example.com", 1)
+ require.NoError(t, err)
+}
+
+func TestClient_DeleteZone(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("delete_zone.xml"),
+ servermock.CheckRequestBodyFromFile("delete_zone-request.xml").IgnoreWhitespace()).
+ Build(t)
+
+ err := client.DeleteZone(t.Context(), 1)
+ require.NoError(t, err)
+}
diff --git a/providers/dns/gandi/internal/fixtures/add_txt_record-request.xml b/providers/dns/gandi/internal/fixtures/add_txt_record-request.xml
new file mode 100644
index 000000000..001ee7a33
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/add_txt_record-request.xml
@@ -0,0 +1,49 @@
+
+
+ domain.zone.record.add
+
+
+ secret
+
+
+
+
+ 1
+
+
+
+
+ 123
+
+
+
+
+
+
+ type
+
+ TXT
+
+
+
+ name
+
+ foo
+
+
+
+ value
+
+ content
+
+
+
+ ttl
+
+ 120
+
+
+
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/clone_zone-request.xml b/providers/dns/gandi/internal/fixtures/clone_zone-request.xml
new file mode 100644
index 000000000..40ee87c7e
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/clone_zone-request.xml
@@ -0,0 +1,31 @@
+
+
+ domain.zone.clone
+
+
+ secret
+
+
+
+
+ 6
+
+
+
+
+ 0
+
+
+
+
+
+
+ name
+
+ foo
+
+
+
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/clone_zone.xml b/providers/dns/gandi/internal/fixtures/clone_zone.xml
new file mode 100644
index 000000000..2af93526e
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/clone_zone.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ id
+
+ 1
+
+
+
+ foo
+
+ 2
+
+
+
+
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/delete_zone-request.xml b/providers/dns/gandi/internal/fixtures/delete_zone-request.xml
new file mode 100644
index 000000000..0ba9cb766
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/delete_zone-request.xml
@@ -0,0 +1,14 @@
+
+
+ domain.zone.delete
+
+
+ secret
+
+
+
+
+ 1
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/delete_zone.xml b/providers/dns/gandi/internal/fixtures/delete_zone.xml
new file mode 100644
index 000000000..28ba00dc5
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/delete_zone.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ true
+
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/empty.xml b/providers/dns/gandi/internal/fixtures/empty.xml
new file mode 100644
index 000000000..7843fd723
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/empty.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/providers/dns/gandi/internal/fixtures/get_zone_id-request.xml b/providers/dns/gandi/internal/fixtures/get_zone_id-request.xml
new file mode 100644
index 000000000..173a725d8
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/get_zone_id-request.xml
@@ -0,0 +1,14 @@
+
+
+ domain.info
+
+
+ secret
+
+
+
+
+ example.com
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/get_zone_id.xml b/providers/dns/gandi/internal/fixtures/get_zone_id.xml
new file mode 100644
index 000000000..2a11e0dff
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/get_zone_id.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ zone_id
+
+ 1
+
+
+
+ foo
+
+ 2
+
+
+
+
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/new_zone_version-request.xml b/providers/dns/gandi/internal/fixtures/new_zone_version-request.xml
new file mode 100644
index 000000000..2fbac82de
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/new_zone_version-request.xml
@@ -0,0 +1,14 @@
+
+
+ domain.zone.version.new
+
+
+ secret
+
+
+
+
+ 6
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/new_zone_version.xml b/providers/dns/gandi/internal/fixtures/new_zone_version.xml
new file mode 100644
index 000000000..feb84e486
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/new_zone_version.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ 1
+
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/set_zone-request.xml b/providers/dns/gandi/internal/fixtures/set_zone-request.xml
new file mode 100644
index 000000000..71ac843fd
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/set_zone-request.xml
@@ -0,0 +1,19 @@
+
+
+ domain.zone.set
+
+
+ secret
+
+
+
+
+ example.com
+
+
+
+
+ 1
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/set_zone.xml b/providers/dns/gandi/internal/fixtures/set_zone.xml
new file mode 100644
index 000000000..2a11e0dff
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/set_zone.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ zone_id
+
+ 1
+
+
+
+ foo
+
+ 2
+
+
+
+
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/set_zone_version-request.xml b/providers/dns/gandi/internal/fixtures/set_zone_version-request.xml
new file mode 100644
index 000000000..68a021446
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/set_zone_version-request.xml
@@ -0,0 +1,19 @@
+
+
+ domain.zone.version.set
+
+
+ secret
+
+
+
+
+ 1
+
+
+
+
+ 123
+
+
+
diff --git a/providers/dns/gandi/internal/fixtures/set_zone_version.xml b/providers/dns/gandi/internal/fixtures/set_zone_version.xml
new file mode 100644
index 000000000..28ba00dc5
--- /dev/null
+++ b/providers/dns/gandi/internal/fixtures/set_zone_version.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ true
+
+
+
+
diff --git a/providers/dns/gandiv5/gandiv5_test.go b/providers/dns/gandiv5/gandiv5_test.go
index 57fed032e..451b1b683 100644
--- a/providers/dns/gandiv5/gandiv5_test.go
+++ b/providers/dns/gandiv5/gandiv5_test.go
@@ -1,15 +1,11 @@
package gandiv5
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "regexp"
"testing"
- "github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
@@ -95,81 +91,32 @@ func TestNewDNSProviderConfig(t *testing.T) {
// TestDNSProvider runs Present and CleanUp against a fake Gandi RPC
// Server, whose responses are predetermined for particular requests.
func TestDNSProvider(t *testing.T) {
- // serverResponses is the JSON Request->Response map used by the
- // fake JSON server.
- serverResponses := map[string]map[string]string{
- http.MethodGet: {
- ``: `{"rrset_ttl":300,"rrset_values":[],"rrset_name":"_acme-challenge.abc.def","rrset_type":"TXT"}`,
+ provider := servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.PersonalAccessToken = "123412341234123412341234"
+ config.BaseURL = server.URL
+
+ return NewDNSProviderConfig(config)
},
- http.MethodPut: {
- `{"rrset_ttl":300,"rrset_values":["TOKEN"]}`: `{"message": "Zone Record Created"}`,
- },
- http.MethodDelete: {
- ``: ``,
- },
- }
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer 123412341234123412341234"),
+ ).
+ Route("GET /domains/example.com/records/_acme-challenge.abc.def/TXT",
+ servermock.RawStringResponse(`{"rrset_ttl":300,"rrset_values":[],"rrset_name":"_acme-challenge.abc.def","rrset_type":"TXT"}`)).
+ Route("PUT /domains/example.com/records/_acme-challenge.abc.def/TXT",
+ servermock.RawStringResponse(`{"message": "Zone Record Created"}`),
+ servermock.CheckRequestJSONBody(`{"rrset_ttl":300,"rrset_values":["ezRpBPY8wH8djMLYjX2uCKPwiKDkFZ1SFMJ6ZXGlHrQ"]}`)).
+ Route("DELETE /domains/example.com/records/_acme-challenge.abc.def/TXT", nil).
+ Build(t)
fakeKeyAuth := "XXXX"
- regexpToken := regexp.MustCompile(`"rrset_values":\[".+"\]`)
-
- // start fake RPC server
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/domains/example.com/records/_acme-challenge.abc.def/TXT", func(rw http.ResponseWriter, req *http.Request) {
- log.Infof("request: %s %s", req.Method, req.URL)
-
- if req.Header.Get("Authorization") != "Bearer 123412341234123412341234" {
- http.Error(rw, `{"message": "missing or malformed Authorization"}`, http.StatusUnauthorized)
- return
- }
-
- if req.Method == http.MethodPost && req.Header.Get("Content-Type") != "application/json" {
- http.Error(rw, `{"message": "invalid content type"}`, http.StatusBadRequest)
- return
- }
-
- body, errS := io.ReadAll(req.Body)
- if errS != nil {
- http.Error(rw, fmt.Sprintf(`{"message": "read body error: %v"}`, errS), http.StatusInternalServerError)
- return
- }
-
- body = regexpToken.ReplaceAllLiteral(body, []byte(`"rrset_values":["TOKEN"]`))
-
- responses, ok := serverResponses[req.Method]
- if !ok {
- http.Error(rw, fmt.Sprintf(`{"message": "Server response for request not found: %#q"}`, string(body)), http.StatusInternalServerError)
- return
- }
-
- resp := responses[string(body)]
-
- _, errS = rw.Write([]byte(resp))
- if errS != nil {
- http.Error(rw, fmt.Sprintf(`{"message": "failed to write response: %v"}`, errS), http.StatusInternalServerError)
- return
- }
- })
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- log.Infof("request: %s %s", req.Method, req.URL)
- http.Error(rw, fmt.Sprintf(`{"message": "URL doesn't match: %s"}`, req.URL), http.StatusNotFound)
- })
-
// define function to override findZoneByFqdn with
fakeFindZoneByFqdn := func(fqdn string) (string, error) {
return "example.com.", nil
}
- config := NewDefaultConfig()
- config.PersonalAccessToken = "123412341234123412341234"
- config.BaseURL = server.URL
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
// override findZoneByFqdn function
savedFindZoneByFqdn := provider.findZoneByFqdn
defer func() {
@@ -178,7 +125,7 @@ func TestDNSProvider(t *testing.T) {
provider.findZoneByFqdn = fakeFindZoneByFqdn
// run Present
- err = provider.Present("abc.def.example.com", "", fakeKeyAuth)
+ err := provider.Present("abc.def.example.com", "", fakeKeyAuth)
require.NoError(t, err)
// run CleanUp
diff --git a/providers/dns/gandiv5/internal/client_test.go b/providers/dns/gandiv5/internal/client_test.go
new file mode 100644
index 000000000..2465566f9
--- /dev/null
+++ b/providers/dns/gandiv5/internal/client_test.go
@@ -0,0 +1,48 @@
+package internal
+
+import (
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
+ "github.com/stretchr/testify/require"
+)
+
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret", "xxx")
+ client.BaseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With("X-Api-Key", "secret").
+ WithAuthorization("Bearer xxx"),
+ )
+}
+
+func TestClient_AddTXTRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /domains/example.com/records/foo/TXT",
+ servermock.ResponseFromFixture("add_txt_record_get.json")).
+ Route("PUT /domains/example.com/records/foo/TXT",
+ servermock.ResponseFromFixture("api_response.json"),
+ servermock.CheckRequestJSONBody(`{"rrset_ttl":120,"rrset_values":["content","value1"]}`)).
+ Build(t)
+
+ err := client.AddTXTRecord(t.Context(), "example.com", "foo", "content", 120)
+ require.NoError(t, err)
+}
+
+func TestClient_DeleteTXTRecord(t *testing.T) {
+ client := mockBuilder().
+ Route("DELETE /domains/example.com/records/foo/TXT",
+ servermock.ResponseFromFixture("api_response.json")).
+ Build(t)
+
+ err := client.DeleteTXTRecord(t.Context(), "example.com", "foo")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/gandiv5/internal/fixtures/add_txt_record_get.json b/providers/dns/gandiv5/internal/fixtures/add_txt_record_get.json
new file mode 100644
index 000000000..fead6ab0a
--- /dev/null
+++ b/providers/dns/gandiv5/internal/fixtures/add_txt_record_get.json
@@ -0,0 +1,8 @@
+{
+ "rrset_ttl": 120,
+ "rrset_values": [
+ "value1"
+ ],
+ "rrset_name": "foo",
+ "rrset_type": "TXT"
+}
diff --git a/providers/dns/gandiv5/internal/fixtures/api_response.json b/providers/dns/gandiv5/internal/fixtures/api_response.json
new file mode 100644
index 000000000..47f4352ff
--- /dev/null
+++ b/providers/dns/gandiv5/internal/fixtures/api_response.json
@@ -0,0 +1,4 @@
+{
+ "message": "test",
+ "uuid": "123456789"
+}
diff --git a/providers/dns/gcloud/googlecloud_test.go b/providers/dns/gcloud/googlecloud_test.go
index 15c61556c..7fda2f8f6 100644
--- a/providers/dns/gcloud/googlecloud_test.go
+++ b/providers/dns/gcloud/googlecloud_test.go
@@ -11,6 +11,7 @@ import (
"time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2/google"
"google.golang.org/api/dns/v1"
@@ -144,245 +145,160 @@ func TestNewDNSProviderConfig(t *testing.T) {
}
func TestPresentNoExistingRR(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ provider := mockBuilder().
+ // getHostedZone
+ Route("GET /dns/v1/projects/manhattan/managedZones",
+ servermock.JSONEncode(&dns.ManagedZonesListResponse{
+ ManagedZones: []*dns.ManagedZone{
+ {Name: "test", Visibility: "public"},
+ },
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("dnsName", "lego.wtf.").
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ // findTxtRecords
+ Route("GET /dns/v1/projects/manhattan/managedZones/test/rrsets",
+ servermock.JSONEncode(&dns.ResourceRecordSetsListResponse{
+ Rrsets: []*dns.ResourceRecordSet{},
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge.lego.wtf.").
+ With("type", "TXT").
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ // applyChanges [Create]
+ Route("POST /dns/v1/projects/manhattan/managedZones/test/changes",
+ http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ var chgReq dns.Change
+ if err := json.NewDecoder(req.Body).Decode(&chgReq); err != nil {
+ http.Error(rw, err.Error(), http.StatusBadRequest)
+ return
+ }
- // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf.
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
+ chgResp := chgReq
+ chgResp.Status = changeStatusDone
- mzlrs := &dns.ManagedZonesListResponse{
- ManagedZones: []*dns.ManagedZone{
- {Name: "test", Visibility: "public"},
- },
- }
-
- err := json.NewEncoder(w).Encode(mzlrs)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- rrslr := &dns.ResourceRecordSetsListResponse{
- Rrsets: []*dns.ResourceRecordSet{},
- }
-
- err := json.NewEncoder(w).Encode(rrslr)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- // applyChanges [Create]: /manhattan/managedZones/test/changes?alt=json
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/changes", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- var chgReq dns.Change
- if err := json.NewDecoder(r.Body).Decode(&chgReq); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- chgResp := chgReq
- chgResp.Status = changeStatusDone
-
- if err := json.NewEncoder(w).Encode(chgResp); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- config := NewDefaultConfig()
- config.HTTPClient = &http.Client{Timeout: 10 * time.Second}
- config.Project = "manhattan"
-
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- p.client.BasePath = server.URL
+ if err := json.NewEncoder(rw).Encode(chgResp); err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ Build(t)
domain := "lego.wtf"
- err = p.Present(domain, "", "")
+ err := provider.Present(domain, "", "")
require.NoError(t, err)
}
func TestPresentWithExistingRR(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf.
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- mzlrs := &dns.ManagedZonesListResponse{
- ManagedZones: []*dns.ManagedZone{
- {Name: "test", Visibility: "public"},
- },
- }
-
- err := json.NewEncoder(w).Encode(mzlrs)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- rrslr := &dns.ResourceRecordSetsListResponse{
- Rrsets: []*dns.ResourceRecordSet{{
- Name: "_acme-challenge.lego.wtf.",
- Rrdatas: []string{`"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`},
- Ttl: 120,
- Type: "TXT",
- }},
- }
-
- err := json.NewEncoder(w).Encode(rrslr)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- // applyChanges [Create]: /manhattan/managedZones/test/changes?alt=json
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/changes", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- var chgReq dns.Change
- if err := json.NewDecoder(r.Body).Decode(&chgReq); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- if len(chgReq.Additions) > 0 {
- sort.Strings(chgReq.Additions[0].Rrdatas)
- }
-
- var prevVal string
- for _, addition := range chgReq.Additions {
- for _, value := range addition.Rrdatas {
- if prevVal == value {
- http.Error(w, fmt.Sprintf("The resource %s already exists", value), http.StatusConflict)
+ provider := mockBuilder().
+ // getHostedZone
+ Route("GET /dns/v1/projects/manhattan/managedZones",
+ servermock.JSONEncode(&dns.ManagedZonesListResponse{
+ ManagedZones: []*dns.ManagedZone{
+ {Name: "test", Visibility: "public"},
+ },
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("dnsName", "lego.wtf.").
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ // findTxtRecords
+ Route("GET /dns/v1/projects/manhattan/managedZones/test/rrsets",
+ servermock.JSONEncode(&dns.ResourceRecordSetsListResponse{
+ Rrsets: []*dns.ResourceRecordSet{{
+ Name: "_acme-challenge.lego.wtf.",
+ Rrdatas: []string{`"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`},
+ Ttl: 120,
+ Type: "TXT",
+ }},
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge.lego.wtf.").
+ With("type", "TXT").
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ // applyChanges [Create]
+ Route("POST /dns/v1/projects/manhattan/managedZones/test/changes",
+ http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ var chgReq dns.Change
+ if err := json.NewDecoder(req.Body).Decode(&chgReq); err != nil {
+ http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
- prevVal = value
- }
- }
- chgResp := chgReq
- chgResp.Status = changeStatusDone
+ if len(chgReq.Additions) > 0 {
+ sort.Strings(chgReq.Additions[0].Rrdatas)
+ }
- if err := json.NewEncoder(w).Encode(chgResp); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ var prevVal string
+ for _, addition := range chgReq.Additions {
+ for _, value := range addition.Rrdatas {
+ if prevVal == value {
+ http.Error(rw, fmt.Sprintf("The resource %s already exists", value), http.StatusConflict)
+ return
+ }
+ prevVal = value
+ }
+ }
- config := NewDefaultConfig()
- config.HTTPClient = &http.Client{Timeout: 10 * time.Second}
- config.Project = "manhattan"
+ chgResp := chgReq
+ chgResp.Status = changeStatusDone
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- p.client.BasePath = server.URL
+ if err := json.NewEncoder(rw).Encode(chgResp); err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ Build(t)
domain := "lego.wtf"
- err = p.Present(domain, "", "")
+ err := provider.Present(domain, "", "")
require.NoError(t, err)
}
func TestPresentSkipExistingRR(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- // getHostedZone: /manhattan/managedZones?alt=json&dnsName=lego.wtf.
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- mzlrs := &dns.ManagedZonesListResponse{
- ManagedZones: []*dns.ManagedZone{
- {Name: "test", Visibility: "public"},
- },
- }
-
- err := json.NewEncoder(w).Encode(mzlrs)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- // findTxtRecords: /manhattan/managedZones/test/rrsets?alt=json&name=_acme-challenge.lego.wtf.&type=TXT
- mux.HandleFunc("/dns/v1/projects/manhattan/managedZones/test/rrsets", func(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- rrslr := &dns.ResourceRecordSetsListResponse{
- Rrsets: []*dns.ResourceRecordSet{{
- Name: "_acme-challenge.lego.wtf.",
- Rrdatas: []string{`"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`},
- Ttl: 120,
- Type: "TXT",
- }},
- }
-
- err := json.NewEncoder(w).Encode(rrslr)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- config := NewDefaultConfig()
- config.HTTPClient = &http.Client{Timeout: 10 * time.Second}
- config.Project = "manhattan"
-
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- p.client.BasePath = server.URL
+ provider := mockBuilder().
+ // getHostedZone
+ Route("GET /dns/v1/projects/manhattan/managedZones",
+ servermock.JSONEncode(&dns.ManagedZonesListResponse{
+ ManagedZones: []*dns.ManagedZone{
+ {Name: "test", Visibility: "public"},
+ },
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("dnsName", "lego.wtf.").
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ // findTxtRecords
+ Route("GET /dns/v1/projects/manhattan/managedZones/test/rrsets",
+ servermock.JSONEncode(&dns.ResourceRecordSetsListResponse{
+ Rrsets: []*dns.ResourceRecordSet{{
+ Name: "_acme-challenge.lego.wtf.",
+ Rrdatas: []string{`"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"X7DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"`, `"huji"`},
+ Ttl: 120,
+ Type: "TXT",
+ }},
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge.lego.wtf.").
+ With("type", "TXT").
+ With("prettyPrint", "false").
+ With("alt", "json")).
+ Build(t)
domain := "lego.wtf"
- err = p.Present(domain, "", "")
+ err := provider.Present(domain, "", "")
require.NoError(t, err)
}
@@ -432,3 +348,20 @@ func TestLiveCleanUp(t *testing.T) {
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
+
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.HTTPClient = &http.Client{Timeout: 10 * time.Second}
+ config.Project = "manhattan"
+
+ p, err := NewDNSProviderConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ p.client.BasePath = server.URL
+
+ return p, err
+ })
+}
diff --git a/providers/dns/gcore/internal/client_test.go b/providers/dns/gcore/internal/client_test.go
index 21bdb8e05..4a0f83311 100644
--- a/providers/dns/gcore/internal/client_test.go
+++ b/providers/dns/gcore/internal/client_test.go
@@ -1,47 +1,41 @@
package internal
import (
- "encoding/json"
- "fmt"
"net/http"
"net/http/httptest"
"net/url"
- "reflect"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
- testToken = "test"
- testRecordContent = "acme"
- testRecordContent2 = "foo"
- testTTL = 10
+ testToken = "test"
+ testRecordContent = "acme"
+ testTTL = 10
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(testToken)
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(testToken)
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders())
}
func TestClient_GetZone(t *testing.T) {
- client, mux := setupTest(t)
-
expected := Zone{Name: "example.com"}
- mux.Handle("/v2/zones/example.com", validationHandler{
- method: http.MethodGet,
- next: handleJSONResponse(expected),
- })
+ client := mockBuilder().
+ Route("GET /v2/zones/example.com",
+ servermock.JSONEncode(expected)).
+ Build(t)
zone, err := client.GetZone(t.Context(), "example.com")
require.NoError(t, err)
@@ -50,20 +44,16 @@ func TestClient_GetZone(t *testing.T) {
}
func TestClient_GetZone_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v2/zones/example.com", validationHandler{
- method: http.MethodGet,
- next: handleAPIError(),
- })
+ client := mockBuilder().
+ Route("GET /v2/zones/example.com",
+ servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
_, err := client.GetZone(t.Context(), "example.com")
- require.Error(t, err)
+ require.EqualError(t, err, "get zone example.com: 500: oops")
}
func TestClient_GetRRSet(t *testing.T) {
- client, mux := setupTest(t)
-
expected := RRSet{
TTL: testTTL,
Records: []Records{
@@ -71,10 +61,10 @@ func TestClient_GetRRSet(t *testing.T) {
},
}
- mux.Handle("/v2/zones/example.com/foo.example.com/TXT", validationHandler{
- method: http.MethodGet,
- next: handleJSONResponse(expected),
- })
+ client := mockBuilder().
+ Route("GET /v2/zones/example.com/foo.example.com/TXT",
+ servermock.JSONEncode(expected)).
+ Build(t)
rrSet, err := client.GetRRSet(t.Context(), "example.com", "foo.example.com")
require.NoError(t, err)
@@ -83,173 +73,93 @@ func TestClient_GetRRSet(t *testing.T) {
}
func TestClient_GetRRSet_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v2/zones/example.com/foo.example.com/TXT", validationHandler{
- method: http.MethodGet,
- next: handleAPIError(),
- })
+ client := mockBuilder().
+ Route("GET /v2/zones/example.com/foo.example.com/TXT",
+ servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
_, err := client.GetRRSet(t.Context(), "example.com", "foo.example.com")
- require.Error(t, err)
+ require.EqualError(t, err, "get txt records example.com -> foo.example.com: 500: oops")
}
func TestClient_DeleteRRSet(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v2/zones/test.example.com/my.test.example.com/"+txtRecordType,
- validationHandler{method: http.MethodDelete})
+ client := mockBuilder().
+ Route("DELETE /v2/zones/test.example.com/my.test.example.com/TXT", nil).
+ Build(t)
err := client.DeleteRRSet(t.Context(), "test.example.com", "my.test.example.com.")
require.NoError(t, err)
}
func TestClient_DeleteRRSet_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/v2/zones/test.example.com/my.test.example.com/"+txtRecordType, validationHandler{
- method: http.MethodDelete,
- next: handleAPIError(),
- })
+ client := mockBuilder().
+ Route("DELETE /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
err := client.DeleteRRSet(t.Context(), "test.example.com", "my.test.example.com.")
require.NoError(t, err)
}
-func TestClient_AddRRSet(t *testing.T) {
- testCases := []struct {
- desc string
- zone string
- recordName string
- value string
- handledDomain string
- handlers map[string]http.Handler
- wantErr bool
- }{
- {
- desc: "success add",
- zone: "test.example.com",
- recordName: "my.test.example.com",
- value: testRecordContent,
- handlers: map[string]http.Handler{
- // createRRSet
- "/v2/zones/test.example.com/my.test.example.com/" + txtRecordType: validationHandler{
- method: http.MethodPost,
- next: handleAddRRSet([]Records{{Content: []string{testRecordContent}}}),
- },
- },
- },
- {
- desc: "success update",
- zone: "test.example.com",
- recordName: "my.test.example.com",
- value: testRecordContent,
- handlers: map[string]http.Handler{
- "/v2/zones/test.example.com/my.test.example.com/" + txtRecordType: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
- switch req.Method {
- case http.MethodGet: // GetRRSet
- data := RRSet{
- TTL: testTTL,
- Records: []Records{{Content: []string{testRecordContent2}}},
- }
- handleJSONResponse(data).ServeHTTP(rw, req)
- case http.MethodPut: // updateRRSet
- expected := []Records{
- {Content: []string{testRecordContent}},
- {Content: []string{testRecordContent2}},
- }
- handleAddRRSet(expected).ServeHTTP(rw, req)
- default:
- http.Error(rw, "wrong method", http.StatusMethodNotAllowed)
- }
- }),
- },
- },
- {
- desc: "not in the zone",
- zone: "test.example.com",
- recordName: "notfound.example.com",
- value: testRecordContent,
- wantErr: true,
- },
- }
+func TestClient_AddRRSet_add(t *testing.T) {
+ client := mockBuilder().
+ // GetRRSet
+ Route("GET /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode(APIError{Message: "not found"}).WithStatusCode(http.StatusBadRequest)).
+ // createRRSet
+ Route("POST /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode([]Records{{Content: []string{testRecordContent}}}),
+ servermock.CheckRequestJSONBody(`{"ttl":10,"resource_records":[{"content":["acme"]}]}`)).
+ Build(t)
- for _, test := range testCases {
- t.Run(test.desc, func(t *testing.T) {
- cl, mux := setupTest(t)
-
- for pattern, handler := range test.handlers {
- mux.Handle(pattern, handler)
- }
-
- err := cl.AddRRSet(t.Context(), test.zone, test.recordName, test.value, testTTL)
- if test.wantErr {
- require.Error(t, err)
- return
- }
-
- require.NoError(t, err)
- })
- }
+ err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL)
+ require.NoError(t, err)
}
-type validationHandler struct {
- method string
- next http.Handler
+func TestClient_AddRRSet_add_error(t *testing.T) {
+ client := mockBuilder().
+ // GetRRSet
+ Route("GET /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode(APIError{Message: "not found"}).WithStatusCode(http.StatusBadRequest)).
+ // createRRSet
+ Route("POST /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusBadRequest)).
+ Build(t)
+
+ err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL)
+ require.EqualError(t, err, "400: oops")
}
-func (v validationHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
- if req.Header.Get(authorizationHeader) != fmt.Sprintf("%s %s", tokenTypeHeader, testToken) {
- rw.WriteHeader(http.StatusForbidden)
- _ = json.NewEncoder(rw).Encode(APIError{Message: "token up for parsing was not passed through the context"})
- return
- }
+func TestClient_AddRRSet_update(t *testing.T) {
+ client := mockBuilder().
+ // GetRRSet
+ Route("GET /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode(RRSet{
+ TTL: testTTL,
+ Records: []Records{{Content: []string{"foo"}}},
+ })).
+ // updateRRSet
+ Route("PUT /v2/zones/test.example.com/my.test.example.com/TXT", nil,
+ servermock.CheckRequestJSONBody(`{"ttl":10,"resource_records":[{"content":["acme"]},{"content":["foo"]}]}`)).
+ Build(t)
- if req.Method != v.method {
- http.Error(rw, "wrong method", http.StatusMethodNotAllowed)
- return
- }
-
- if v.next != nil {
- v.next.ServeHTTP(rw, req)
- }
+ err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL)
+ require.NoError(t, err)
}
-func handleAPIError() http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- rw.WriteHeader(http.StatusInternalServerError)
- _ = json.NewEncoder(rw).Encode(APIError{Message: "oops"})
- }
-}
-
-func handleJSONResponse(data any) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- err := json.NewEncoder(rw).Encode(data)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
-
-func handleAddRRSet(expected []Records) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- body := RRSet{}
-
- err := json.NewDecoder(req.Body).Decode(&body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if body.TTL != testTTL {
- http.Error(rw, "wrong ttl", http.StatusInternalServerError)
- return
- }
-
- if !reflect.DeepEqual(body.Records, expected) {
- http.Error(rw, "wrong resource records", http.StatusInternalServerError)
- return
- }
- }
+func TestClient_AddRRSet_update_error(t *testing.T) {
+ client := mockBuilder().
+ // GetRRSet
+ Route("GET /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode(RRSet{
+ TTL: testTTL,
+ Records: []Records{{Content: []string{"foo"}}},
+ })).
+ // updateRRSet
+ Route("PUT /v2/zones/test.example.com/my.test.example.com/TXT",
+ servermock.JSONEncode(APIError{Message: "oops"}).WithStatusCode(http.StatusBadRequest)).
+ Build(t)
+
+ err := client.AddRRSet(t.Context(), "test.example.com", "my.test.example.com", testRecordContent, testTTL)
+ require.EqualError(t, err, "400: oops")
}
diff --git a/providers/dns/glesys/internal/client_test.go b/providers/dns/glesys/internal/client_test.go
index ab30f9516..cd71757ff 100644
--- a/providers/dns/glesys/internal/client_test.go
+++ b/providers/dns/glesys/internal/client_test.go
@@ -1,68 +1,35 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- apiUser, apiKey, ok := req.BasicAuth()
- if apiUser != "user" || apiKey != "secret" || !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("user", "secret"),
+ )
}
func TestClient_AddTXTRecord(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/domain/addrecord", http.StatusOK, "add-record.json")
+ client := mockBuilder().
+ Route("POST /domain/addrecord",
+ servermock.ResponseFromFixture("add-record.json"),
+ servermock.CheckRequestJSONBody(`{"domainname":"example.com","host":"foo","type":"TXT","data":"txt","ttl":120}`)).
+ Build(t)
recordID, err := client.AddTXTRecord(t.Context(), "example.com", "foo", "txt", 120)
require.NoError(t, err)
@@ -71,7 +38,11 @@ func TestClient_AddTXTRecord(t *testing.T) {
}
func TestClient_DeleteTXTRecord(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/domain/deleterecord", http.StatusOK, "delete-record.json")
+ client := mockBuilder().
+ Route("POST /domain/deleterecord",
+ servermock.ResponseFromFixture("delete-record.json"),
+ servermock.CheckRequestJSONBody(`{"recordid":123}`)).
+ Build(t)
err := client.DeleteTXTRecord(t.Context(), 123)
require.NoError(t, err)
diff --git a/providers/dns/godaddy/internal/client_test.go b/providers/dns/godaddy/internal/client_test.go
index 64bf2b388..741a55f3e 100644
--- a/providers/dns/godaddy/internal/client_test.go
+++ b/providers/dns/godaddy/internal/client_test.go
@@ -1,37 +1,33 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("key", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("key", "secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("sso-key key:secret"))
}
func TestClient_GetRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusOK, "getrecords.json"))
+ client := mockBuilder().
+ Route("GET /v1/domains/example.com/records/TXT/", servermock.ResponseFromFixture("getrecords.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "example.com", "TXT", "")
require.NoError(t, err)
@@ -49,9 +45,10 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_errors(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/example.com/records/TXT/", testHandler(http.MethodGet, http.StatusUnprocessableEntity, "errors.json"))
+ client := mockBuilder().
+ Route("GET /v1/domains/example.com/records/TXT/",
+ servermock.ResponseFromFixture("errors.json").WithStatusCode(http.StatusUnprocessableEntity)).
+ Build(t)
records, err := client.GetRecords(t.Context(), "example.com", "TXT", "")
require.EqualError(t, err, "[status code: 422] INVALID_BODY: Request body doesn't fulfill schema, see details in `fields`")
@@ -59,20 +56,10 @@ func TestClient_GetRecords_errors(t *testing.T) {
}
func TestClient_UpdateTxtRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/example.com/records/TXT/lego", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPut {
- http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "sso-key key:secret" {
- http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized)
- return
- }
- })
+ client := mockBuilder().
+ Route("PUT /v1/domains/example.com/records/TXT/lego", nil,
+ servermock.CheckRequestJSONBodyFromFile("update_records-request.json")).
+ Build(t)
records := []DNSRecord{
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
@@ -88,10 +75,11 @@ func TestClient_UpdateTxtRecords(t *testing.T) {
}
func TestClient_UpdateTxtRecords_errors(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/example.com/records/TXT/lego",
- testHandler(http.MethodPut, http.StatusUnprocessableEntity, "errors.json"))
+ client := mockBuilder().
+ Route("PUT /v1/domains/example.com/records/TXT/lego",
+ servermock.ResponseFromFixture("errors.json").WithStatusCode(http.StatusUnprocessableEntity),
+ servermock.CheckRequestJSONBodyFromFile("update_records-request.json")).
+ Build(t)
records := []DNSRecord{
{Name: "_acme-challenge", Type: "TXT", Data: " ", TTL: 600},
@@ -107,54 +95,21 @@ func TestClient_UpdateTxtRecords_errors(t *testing.T) {
}
func TestClient_DeleteTxtRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/example.com/records/TXT/foo", testHandler(http.MethodDelete, http.StatusNoContent, ""))
+ client := mockBuilder().
+ Route("DELETE /v1/domains/example.com/records/TXT/foo",
+ servermock.Noop().WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.DeleteTxtRecords(t.Context(), "example.com", "foo")
require.NoError(t, err)
}
func TestClient_DeleteTxtRecords_errors(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/domains/example.com/records/TXT/foo", testHandler(http.MethodDelete, http.StatusConflict, "error-extended.json"))
+ client := mockBuilder().
+ Route("DELETE /v1/domains/example.com/records/TXT/foo",
+ servermock.ResponseFromFixture("error-extended.json").WithStatusCode(http.StatusConflict)).
+ Build(t)
err := client.DeleteTxtRecords(t.Context(), "example.com", "foo")
require.EqualError(t, err, "[status code: 409] ACCESS_DENIED: Authenticated user is not allowed access [test: content (path=/foo) (pathRelated=/bar)]")
}
-
-func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "sso-key key:secret" {
- http.Error(rw, fmt.Sprintf("invalid API key or secret: %s", auth), http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(statusCode)
-
- if statusCode == http.StatusNoContent {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
- }
-}
diff --git a/providers/dns/godaddy/internal/fixtures/update_records-request.json b/providers/dns/godaddy/internal/fixtures/update_records-request.json
new file mode 100644
index 000000000..969afb2dc
--- /dev/null
+++ b/providers/dns/godaddy/internal/fixtures/update_records-request.json
@@ -0,0 +1,38 @@
+[
+ {
+ "name": "_acme-challenge",
+ "type": "TXT",
+ "data": " ",
+ "ttl": 600
+ },
+ {
+ "name": "_acme-challenge.example",
+ "type": "TXT",
+ "data": "6rrai7-jm7l3PxL4WGmGoS6VMeefSHx24r-qCvUIOxU",
+ "ttl": 600
+ },
+ {
+ "name": "_acme-challenge.example",
+ "type": "TXT",
+ "data": "8Axt-PXQvjOVD2oi2YXqyyn8U5CDcC8P-BphlCxk3Ek",
+ "ttl": 600
+ },
+ {
+ "name": "_acme-challenge.lego",
+ "type": "TXT",
+ "data": " ",
+ "ttl": 600
+ },
+ {
+ "name": "_acme-challenge.lego",
+ "type": "TXT",
+ "data": "0Ad60wO_yxxJPFPb1deir6lQ37FPLeA02YCobo7Qm8A",
+ "ttl": 600
+ },
+ {
+ "name": "_acme-challenge.lego",
+ "type": "TXT",
+ "data": "acme",
+ "ttl": 600
+ }
+]
diff --git a/providers/dns/hetzner/internal/client_test.go b/providers/dns/hetzner/internal/client_test.go
index fe9f992fb..d301493a9 100644
--- a/providers/dns/hetzner/internal/client_test.go
+++ b/providers/dns/hetzner/internal/client_test.go
@@ -1,107 +1,60 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, apiKey string) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder(apiKey string) *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(apiKey)
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(apiKey)
- client.baseURL, _ = url.Parse(server.URL)
- client.HTTPClient = server.Client()
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With(authHeader, apiKey))
}
func TestClient_GetTxtRecord(t *testing.T) {
const zoneID = "zoneA"
- const apiKey = "myKeyA"
- client, mux := setupTest(t, apiKey)
-
- mux.HandleFunc("/api/v1/records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authHeader)
- if auth != apiKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
-
- zID := req.URL.Query().Get("zone_id")
- if zID != zoneID {
- http.Error(rw, fmt.Sprintf("invalid zone ID: %s", zID), http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/get_txt_record.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder("myKeyA").
+ Route("GET /api/v1/records", servermock.ResponseFromFixture("get_txt_record.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("zone_id", zoneID)).
+ Build(t)
record, err := client.GetTxtRecord(t.Context(), "test1", "txttxttxt", zoneID)
require.NoError(t, err)
- fmt.Println(record)
+ expected := &DNSRecord{
+ ID: "1b",
+ Name: "test1",
+ Type: "TXT",
+ Value: "txttxttxt",
+ Priority: 0,
+ TTL: 600,
+ ZoneID: "zoneA",
+ }
+
+ assert.Equal(t, expected, record)
}
func TestClient_CreateRecord(t *testing.T) {
const zoneID = "zoneA"
- const apiKey = "myKeyB"
- client, mux := setupTest(t, apiKey)
-
- mux.HandleFunc("/api/v1/records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authHeader)
- if auth != apiKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open("./fixtures/create_txt_record.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder("myKeyB").
+ Route("POST /api/v1/records", servermock.ResponseFromFixture("create_txt_record.json"),
+ servermock.CheckRequestJSONBodyFromFile("create_txt_record-request.json")).
+ Build(t)
record := DNSRecord{
Name: "test",
@@ -116,57 +69,18 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- const apiKey = "myKeyC"
-
- client, mux := setupTest(t, apiKey)
-
- mux.HandleFunc("/api/v1/records/recordID", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authHeader)
- if auth != apiKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
- })
+ client := mockBuilder("myKeyC").
+ Route("DELETE /api/v1/records/recordID", nil).
+ Build(t)
err := client.DeleteRecord(t.Context(), "recordID")
require.NoError(t, err)
}
func TestClient_GetZoneID(t *testing.T) {
- const apiKey = "myKeyD"
-
- client, mux := setupTest(t, apiKey)
-
- mux.HandleFunc("/api/v1/zones", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authHeader)
- if auth != apiKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open("./fixtures/get_zone_id.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder("myKeyD").
+ Route("GET /api/v1/zones", servermock.ResponseFromFixture("get_zone_id.json")).
+ Build(t)
zoneID, err := client.GetZoneID(t.Context(), "example.com")
require.NoError(t, err)
diff --git a/providers/dns/hetzner/internal/fixtures/create_txt_record-request.json b/providers/dns/hetzner/internal/fixtures/create_txt_record-request.json
new file mode 100644
index 000000000..894d81886
--- /dev/null
+++ b/providers/dns/hetzner/internal/fixtures/create_txt_record-request.json
@@ -0,0 +1,7 @@
+{
+ "name": "test",
+ "type": "TXT",
+ "value": "txttxttxt",
+ "ttl": 600,
+ "zone_id": "zoneA"
+}
diff --git a/providers/dns/hosttech/internal/client_test.go b/providers/dns/hosttech/internal/client_test.go
index 3acbaafc5..223a0d9cf 100644
--- a/providers/dns/hosttech/internal/client_test.go
+++ b/providers/dns/hosttech/internal/client_test.go
@@ -1,23 +1,38 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testAPIKey = "secret"
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey))
+ client.baseURL, _ = url.Parse(server.URL)
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer secret"))
+}
+
func TestClient_GetZones(t *testing.T) {
- client := setupTest(t, "/user/v1/zones", testHandler(http.MethodGet, http.StatusOK, "zones.json"))
+ client := mockBuilder().
+ Route("GET /user/v1/zones",
+ servermock.ResponseFromFixture("zones.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("limit", "100").
+ With("query", "")).
+ Build(t)
zones, err := client.GetZones(t.Context(), "", 100, 0)
require.NoError(t, err)
@@ -38,14 +53,21 @@ func TestClient_GetZones(t *testing.T) {
}
func TestClient_GetZones_error(t *testing.T) {
- client := setupTest(t, "/user/v1/zones", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("GET /user/v1/zones",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetZones(t.Context(), "", 100, 0)
require.Error(t, err)
}
func TestClient_GetZone(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123", testHandler(http.MethodGet, http.StatusOK, "zone.json"))
+ client := mockBuilder().
+ Route("GET /user/v1/zones/123",
+ servermock.ResponseFromFixture("zone.json")).
+ Build(t)
zone, err := client.GetZone(t.Context(), "123")
require.NoError(t, err)
@@ -64,14 +86,23 @@ func TestClient_GetZone(t *testing.T) {
}
func TestClient_GetZone_error(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("GET /user/v1/zones/123",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetZone(t.Context(), "123")
- require.Error(t, err)
+ require.EqualError(t, err, "401: Unauthenticated.")
}
func TestClient_GetRecords(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodGet, http.StatusOK, "records.json"))
+ client := mockBuilder().
+ Route("GET /user/v1/zones/123/records",
+ servermock.ResponseFromFixture("records.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("type", "TXT")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "123", "TXT")
require.NoError(t, err)
@@ -151,14 +182,22 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_error(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodGet, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("GET /user/v1/zones/123/records",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetRecords(t.Context(), "123", "TXT")
- require.Error(t, err)
+ require.EqualError(t, err, "401: Unauthenticated.")
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodPost, http.StatusCreated, "record.json"))
+ client := mockBuilder().
+ Route("POST /user/v1/zones/123/records",
+ servermock.ResponseFromFixture("record.json").
+ WithStatusCode(http.StatusCreated)).
+ Build(t)
record := Record{
Type: "TXT",
@@ -184,7 +223,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123/records", testHandler(http.MethodPost, http.StatusUnauthorized, "error-details.json"))
+ client := mockBuilder().
+ Route("POST /user/v1/zones/123/records",
+ servermock.ResponseFromFixture("error-details.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
record := Record{
Type: "TXT",
@@ -195,68 +238,27 @@ func TestClient_AddRecord_error(t *testing.T) {
}
_, err := client.AddRecord(t.Context(), "123", record)
- require.Error(t, err)
+ require.EqualError(t, err, "401: The given data was invalid. type: [Darf nicht leer sein.]")
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123/records/6", testHandler(http.MethodDelete, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("DELETE /user/v1/zones/123/records/6",
+ servermock.Noop().WithStatusCode(http.StatusNoContent).
+ WithStatusCode(http.StatusCreated)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "123", "6")
require.Error(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "/user/v1/zones/123/records/6", testHandler(http.MethodDelete, http.StatusNoContent, ""))
+ client := mockBuilder().
+ Route("DELETE /user/v1/zones/123/records/6",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "123", "6")
- require.NoError(t, err)
-}
-
-func setupTest(t *testing.T, path string, handler http.Handler) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.Handle(path, handler)
-
- client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey))
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
-}
-
-func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- if req.Header.Get("Authorization") != "Bearer "+testAPIKey {
- http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(statusCode)
-
- if statusCode == http.StatusNoContent {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
- }
+ require.EqualError(t, err, "401: Unauthenticated.")
}
diff --git a/providers/dns/httpreq/httpreq_test.go b/providers/dns/httpreq/httpreq_test.go
index 8dc36ccc6..038b21b1a 100644
--- a/providers/dns/httpreq/httpreq_test.go
+++ b/providers/dns/httpreq/httpreq_test.go
@@ -1,15 +1,12 @@
package httpreq
import (
- "encoding/json"
- "fmt"
- "net/http"
"net/http/httptest"
"net/url"
- "path"
"testing"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
@@ -102,75 +99,60 @@ func TestNewDNSProvider_Present(t *testing.T) {
testCases := []struct {
desc string
- mode string
- username string
- password string
- pathPrefix string
- handler http.HandlerFunc
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "success",
- handler: successHandler,
+ desc: "success",
+ builder: mockBuilder("").
+ Route("/present",
+ servermock.RawStringResponse("lego"),
+ servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)),
},
{
- desc: "success with path prefix",
- handler: successHandler,
- pathPrefix: "/api/acme/",
+ desc: "success with path prefix",
+ builder: mockBuilderWithPathPrefix("", "/api/acme/").
+ Route("/api/acme/present",
+ servermock.RawStringResponse("lego"),
+ servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)),
},
{
desc: "error",
- handler: http.NotFound,
+ builder: mockBuilder(""),
expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found",
},
{
- desc: "success raw mode",
- mode: "RAW",
- handler: successRawModeHandler,
+ desc: "success raw mode",
+ builder: mockBuilder("RAW").
+ Route("/present",
+ servermock.RawStringResponse("lego"),
+ servermock.CheckRequestBody(`{"domain":"domain","token":"token","keyAuth":"key"}`)),
},
{
desc: "error raw mode",
- mode: "RAW",
- handler: http.NotFound,
+ builder: mockBuilder("RAW"),
expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found",
},
{
- desc: "basic auth",
- username: "bar",
- password: "foo",
- handler: func(rw http.ResponseWriter, req *http.Request) {
- username, password, ok := req.BasicAuth()
- if username != "bar" || password != "foo" || !ok {
- rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and password."))
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- fmt.Fprint(rw, "lego")
- },
+ desc: "basic auth fail",
+ builder: mockBuilderWithBasicAuth("nope", "nope").
+ Route("/present", servermock.Noop()),
+ expectedError: `httpreq: unexpected status code: [status code: 400] body: invalid credentials: got [username: "nope", password: "nope"], want [username: "user", password: "secret"]`,
+ },
+ {
+ desc: "basic auth success",
+ builder: mockBuilderWithBasicAuth("user", "secret").
+ Route("/present",
+ servermock.RawStringResponse("lego"),
+ servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
+ p := test.builder.Build(t)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(path.Join("/", test.pathPrefix, "present"), test.handler)
-
- config := NewDefaultConfig()
- config.Endpoint = mustParse(server.URL + test.pathPrefix)
- config.Mode = test.mode
- config.Username = test.username
- config.Password = test.password
-
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- err = p.Present("domain", "token", "key")
+ err := p.Present("domain", "token", "key")
if test.expectedError == "" {
require.NoError(t, err)
} else {
@@ -185,68 +167,53 @@ func TestNewDNSProvider_Cleanup(t *testing.T) {
testCases := []struct {
desc string
- mode string
- username string
- password string
- handler http.HandlerFunc
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "success",
- handler: successHandler,
+ desc: "success",
+ builder: mockBuilder("").
+ Route("/cleanup",
+ servermock.RawStringResponse("lego"),
+ servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)),
},
{
desc: "error",
- handler: http.NotFound,
+ builder: mockBuilder(""),
expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found",
},
{
- desc: "success raw mode",
- mode: "RAW",
- handler: successRawModeHandler,
+ desc: "success raw mode",
+ builder: mockBuilder("RAW").
+ Route("/cleanup",
+ servermock.RawStringResponse("lego"),
+ servermock.CheckRequestBody(`{"domain":"domain","token":"token","keyAuth":"key"}`)),
},
{
desc: "error raw mode",
- mode: "RAW",
- handler: http.NotFound,
+ builder: mockBuilder("RAW"),
expectedError: "httpreq: unexpected status code: [status code: 404] body: 404 page not found",
},
{
- desc: "basic auth",
- username: "bar",
- password: "foo",
- handler: func(rw http.ResponseWriter, req *http.Request) {
- username, password, ok := req.BasicAuth()
- if username != "bar" || password != "foo" || !ok {
- rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and password."))
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
- fmt.Fprint(rw, "lego")
- },
+ desc: "basic auth fail",
+ builder: mockBuilderWithBasicAuth("test", "example").
+ Route("/cleanup", servermock.Noop()),
+ expectedError: `httpreq: unexpected status code: [status code: 400] body: invalid credentials: got [username: "test", password: "example"], want [username: "user", password: "secret"]`,
+ },
+ {
+ desc: "basic auth success",
+ builder: mockBuilderWithBasicAuth("user", "secret").
+ Route("/cleanup",
+ servermock.RawStringResponse("lego"),
+ servermock.CheckRequestJSONBody(`{"fqdn":"_acme-challenge.domain.","value":"LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM"}`)),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
+ p := test.builder.Build(t)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/cleanup", test.handler)
-
- config := NewDefaultConfig()
- config.Endpoint = mustParse(server.URL)
- config.Mode = test.mode
- config.Username = test.username
- config.Password = test.password
-
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- err = p.CleanUp("domain", "token", "key")
+ err := p.CleanUp("domain", "token", "key")
if test.expectedError == "" {
require.NoError(t, err)
} else {
@@ -256,36 +223,39 @@ func TestNewDNSProvider_Cleanup(t *testing.T) {
}
}
-func successHandler(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
+func mockBuilder(mode string) *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.Endpoint, _ = url.Parse(server.URL)
+ config.Mode = mode
- msg := &message{}
- err := json.NewDecoder(req.Body).Decode(msg)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- fmt.Fprint(rw, "lego")
+ return NewDNSProviderConfig(config)
+ })
}
-func successRawModeHandler(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
+func mockBuilderWithPathPrefix(mode, prefix string) *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.Endpoint, _ = url.Parse(server.URL + prefix)
+ config.Mode = mode
- msg := &messageRaw{}
- err := json.NewDecoder(req.Body).Decode(msg)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
+ return NewDNSProviderConfig(config)
+ })
+}
- fmt.Fprint(rw, "lego")
+func mockBuilderWithBasicAuth(username, password string) *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.Endpoint, _ = url.Parse(server.URL)
+ config.Username = username
+ config.Password = password
+
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().WithBasicAuth("user", "secret"))
}
func mustParse(rawURL string) *url.URL {
diff --git a/providers/dns/hurricane/internal/client_test.go b/providers/dns/hurricane/internal/client_test.go
index 2e55c2057..d93f3e0ed 100644
--- a/providers/dns/hurricane/internal/client_test.go
+++ b/providers/dns/hurricane/internal/client_test.go
@@ -1,14 +1,21 @@
package internal
import (
- "fmt"
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
)
+func setupClient(server *httptest.Server) (*Client, error) {
+ client := NewClient(map[string]string{"example.com": "secret"})
+ client.baseURL = server.URL
+ client.HTTPClient = server.Client()
+
+ return client, nil
+}
+
func TestClient_UpdateTxtRecord(t *testing.T) {
testCases := []struct {
code string
@@ -48,31 +55,14 @@ func TestClient_UpdateTxtRecord(t *testing.T) {
t.Run(test.code, func(t *testing.T) {
t.Parallel()
- handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- if err := req.ParseForm(); err != nil {
- http.Error(rw, "failed to parse form data", http.StatusBadRequest)
- return
- }
-
- if req.PostForm.Encode() != "hostname=_acme-challenge.example.com&password=secret&txt=foo" {
- http.Error(rw, "invalid form data", http.StatusBadRequest)
- return
- }
-
- _, _ = rw.Write([]byte(test.code))
- })
-
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- client := NewClient(map[string]string{"example.com": "secret"})
- client.baseURL = server.URL
- client.HTTPClient = server.Client()
+ client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithContentTypeFromURLEncoded()).
+ Route("POST /",
+ servermock.RawStringResponse(test.code),
+ servermock.CheckForm().Strict().
+ With("hostname", "_acme-challenge.example.com").
+ With("password", "secret").
+ With("txt", "foo")).
+ Build(t)
err := client.UpdateTxtRecord(t.Context(), "_acme-challenge.example.com", "foo")
test.expected(t, err)
diff --git a/providers/dns/hyperone/internal/client_test.go b/providers/dns/hyperone/internal/client_test.go
index 55c43700c..aa087c4f2 100644
--- a/providers/dns/hyperone/internal/client_test.go
+++ b/providers/dns/hyperone/internal/client_test.go
@@ -1,16 +1,10 @@
package internal
import (
- "bytes"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -21,8 +15,32 @@ func (s signerMock) GetJWT() (string, error) {
return "", nil
}
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ passport := &Passport{
+ SubjectID: "/iam/project/proj123/sa/xxxxxxx",
+ }
+
+ client, err := NewClient(server.URL, "loc123", passport)
+ if err != nil {
+ return nil, err
+ }
+
+ client.HTTPClient = server.Client()
+ client.signer = signerMock{}
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer"))
+}
+
func TestClient_FindRecordset(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/zone321/recordset", respFromFile("recordset.json"))
+ client := mockBuilder().
+ Route("GET /dns/loc123/project/proj123/zone/zone321/recordset",
+ servermock.ResponseFromFixture("recordset.json")).
+ Build(t)
recordset, err := client.FindRecordset(t.Context(), "zone321", "SOA", "example.com.")
require.NoError(t, err)
@@ -45,8 +63,11 @@ func TestClient_CreateRecordset(t *testing.T) {
Record: &Record{Content: "value"},
}
- client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/zone123/recordset",
- hasReqBody(expectedReqBody), respFromFile("createRecordset.json"))
+ client := mockBuilder().
+ Route("POST /dns/loc123/project/proj123/zone/zone123/recordset",
+ servermock.ResponseFromFixture("createRecordset.json"),
+ servermock.CheckRequestJSONBodyFromStruct(expectedReqBody)).
+ Build(t)
rs, err := client.CreateRecordset(t.Context(), "zone123", "TXT", "test.example.com.", "value", 3600)
require.NoError(t, err)
@@ -56,14 +77,19 @@ func TestClient_CreateRecordset(t *testing.T) {
}
func TestClient_DeleteRecordset(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/zone321/recordset/rs322")
+ client := mockBuilder().
+ Route("DELETE /dns/loc123/project/proj123/zone/zone321/recordset/rs322", nil).
+ Build(t)
err := client.DeleteRecordset(t.Context(), "zone321", "rs322")
require.NoError(t, err)
}
func TestClient_GetRecords(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone/321/recordset/322/record", respFromFile("record.json"))
+ client := mockBuilder().
+ Route("GET /dns/loc123/project/proj123/zone/321/recordset/322/record",
+ servermock.ResponseFromFixture("record.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "321", "322")
require.NoError(t, err)
@@ -84,8 +110,11 @@ func TestClient_CreateRecord(t *testing.T) {
Content: "value",
}
- client := setupTest(t, http.MethodPost, "/dns/loc123/project/proj123/zone/z123/recordset/rs325/record",
- hasReqBody(expectedReqBody), respFromFile("createRecord.json"))
+ client := mockBuilder().
+ Route("POST /dns/loc123/project/proj123/zone/z123/recordset/rs325/record",
+ servermock.ResponseFromFixture("createRecord.json"),
+ servermock.CheckRequestJSONBodyFromStruct(expectedReqBody)).
+ Build(t)
rs, err := client.CreateRecord(t.Context(), "z123", "rs325", "value")
require.NoError(t, err)
@@ -95,14 +124,20 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns/loc123/project/proj123/zone/321/recordset/322/record/323")
+ client := mockBuilder().
+ Route("DELETE /dns/loc123/project/proj123/zone/321/recordset/322/record/323",
+ servermock.ResponseFromFixture("createRecord.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "321", "322", "323")
require.NoError(t, err)
}
func TestClient_FindZone(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json"))
+ client := mockBuilder().
+ Route("GET /dns/loc123/project/proj123/zone",
+ servermock.ResponseFromFixture("zones.json")).
+ Build(t)
zone, err := client.FindZone(t.Context(), "example.com")
require.NoError(t, err)
@@ -119,7 +154,10 @@ func TestClient_FindZone(t *testing.T) {
}
func TestClient_GetZones(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/dns/loc123/project/proj123/zone", respFromFile("zones.json"))
+ client := mockBuilder().
+ Route("GET /dns/loc123/project/proj123/zone",
+ servermock.ResponseFromFixture("zones.json")).
+ Build(t)
zones, err := client.GetZones(t.Context())
require.NoError(t, err)
@@ -143,77 +181,3 @@ func TestClient_GetZones(t *testing.T) {
assert.Equal(t, expected, zones)
}
-
-func setupTest(t *testing.T, method, path string, handlers ...assertHandler) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.Handle(path, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- if len(handlers) != 0 {
- for _, handler := range handlers {
- code, err := handler(rw, req)
- if err != nil {
- http.Error(rw, err.Error(), code)
- return
- }
- }
- }
- }))
-
- passport := &Passport{
- SubjectID: "/iam/project/proj123/sa/xxxxxxx",
- }
-
- client, err := NewClient(server.URL, "loc123", passport)
- require.NoError(t, err)
-
- client.signer = signerMock{}
-
- return client
-}
-
-type assertHandler func(http.ResponseWriter, *http.Request) (int, error)
-
-func hasReqBody(v any) assertHandler {
- return func(rw http.ResponseWriter, req *http.Request) (int, error) {
- reqBody, err := io.ReadAll(req.Body)
- if err != nil {
- return http.StatusBadRequest, err
- }
-
- marshal, err := json.Marshal(v)
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- if !bytes.Equal(marshal, bytes.TrimSpace(reqBody)) {
- return http.StatusBadRequest, fmt.Errorf("invalid request body, got: %s, expect: %s", string(reqBody), string(marshal))
- }
-
- return http.StatusOK, nil
- }
-}
-
-func respFromFile(fixtureName string) assertHandler {
- return func(rw http.ResponseWriter, req *http.Request) (int, error) {
- file, err := os.Open(filepath.Join(".", "fixtures", fixtureName))
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- _, err = io.Copy(rw, file)
- if err != nil {
- return http.StatusInternalServerError, err
- }
-
- return http.StatusOK, nil
- }
-}
diff --git a/providers/dns/infomaniak/internal/client_test.go b/providers/dns/infomaniak/internal/client_test.go
index 5c2d93202..566ed9c34 100644
--- a/providers/dns/infomaniak/internal/client_test.go
+++ b/providers/dns/infomaniak/internal/client_test.go
@@ -1,64 +1,34 @@
package internal
import (
- "bytes"
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := New(OAuthStaticAccessToken(server.Client(), "token"), server.URL)
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client, err := New(OAuthStaticAccessToken(server.Client(), "token"), server.URL)
- require.NoError(t, err)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer token"))
}
func TestClient_CreateDNSRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/1/domain/666/dns/record", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- if req.Header.Get("Authorization") != "Bearer token" {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- raw, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
- defer func() { _ = req.Body.Close() }()
-
- if string(bytes.TrimSpace(raw)) != `{"source":"foo","type":"TXT","ttl":60,"target":"txtxtxttxt"}` {
- http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
- return
- }
-
- response := `{"result":"success","data": "123"}`
-
- _, err = rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /1/domain/666/dns/record",
+ servermock.RawStringResponse(`{"result":"success","data": "123"}`),
+ servermock.CheckRequestJSONBodyFromFile("create_dns_record-request.json")).
+ Build(t)
domain := &DNSDomain{
ID: 666,
@@ -79,53 +49,13 @@ func TestClient_CreateDNSRecord(t *testing.T) {
}
func TestClient_GetDomainByName(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/1/product", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- if req.Header.Get("Authorization") != "Bearer token" {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- serviceName := req.URL.Query().Get("service_name")
- if serviceName != "domain" {
- http.Error(rw, fmt.Sprintf("invalid service_name: %s", serviceName), http.StatusBadRequest)
- return
- }
-
- customerName := req.URL.Query().Get("customer_name")
- if customerName == "" {
- http.Error(rw, fmt.Sprintf("invalid customer_name: %s", customerName), http.StatusBadRequest)
- return
- }
-
- response := `
- {
- "result": "success",
- "data": [
- {
- "id": 123,
- "customer_name": "two.three.example.com"
- },
- {
- "id": 456,
- "customer_name": "three.example.com"
- }
- ]
- }
- `
-
- _, err := rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /1/product",
+ servermock.ResponseFromFixture("get_domain_name.json"),
+ servermock.CheckQueryParameter().Strict().
+ WithRegexp("customer_name", `.+\.example\.com`).
+ With("service_name", "domain")).
+ Build(t)
domain, err := client.GetDomainByName(t.Context(), "one.two.three.example.com.")
require.NoError(t, err)
@@ -135,25 +65,10 @@ func TestClient_GetDomainByName(t *testing.T) {
}
func TestClient_DeleteDNSRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/1/domain/123/dns/record/456", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- if req.Header.Get("Authorization") != "Bearer token" {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- _, err := rw.Write([]byte((`{"result":"success"}`)))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("DELETE /1/domain/123/dns/record/456",
+ servermock.RawStringResponse(`{"result":"success"}`)).
+ Build(t)
err := client.DeleteDNSRecord(t.Context(), 123, "456")
require.NoError(t, err)
diff --git a/providers/dns/infomaniak/internal/fixtures/create_dns_record-request.json b/providers/dns/infomaniak/internal/fixtures/create_dns_record-request.json
new file mode 100644
index 000000000..7e00434f1
--- /dev/null
+++ b/providers/dns/infomaniak/internal/fixtures/create_dns_record-request.json
@@ -0,0 +1,6 @@
+{
+ "source": "foo",
+ "type": "TXT",
+ "ttl": 60,
+ "target": "txtxtxttxt"
+}
diff --git a/providers/dns/infomaniak/internal/fixtures/get_domain_name.json b/providers/dns/infomaniak/internal/fixtures/get_domain_name.json
new file mode 100644
index 000000000..d431cc0d7
--- /dev/null
+++ b/providers/dns/infomaniak/internal/fixtures/get_domain_name.json
@@ -0,0 +1,13 @@
+{
+ "result": "success",
+ "data": [
+ {
+ "id": 123,
+ "customer_name": "two.three.example.com"
+ },
+ {
+ "id": 456,
+ "customer_name": "three.example.com"
+ }
+ ]
+}
diff --git a/providers/dns/internal/active24/client_test.go b/providers/dns/internal/active24/client_test.go
index d92ec574d..ad2a8126b 100644
--- a/providers/dns/internal/active24/client_test.go
+++ b/providers/dns/internal/active24/client_test.go
@@ -1,59 +1,41 @@
package active24
import (
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
"time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("example.com", "user", "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("example.com", "user", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithRegexp("Authorization", `Basic .+`).
+ WithRegexp("Date", `\d+-\d+-\d+T\d{2}:\d{2}:\d{2}.*`).
+ With("Accept-Language", "en_us"))
}
func TestClient_GetServices(t *testing.T) {
- client := setupTest(t, "GET /v1/user/self/service", http.StatusOK, "services.json")
+ client := mockBuilder().
+ Route("GET /v1/user/self/service",
+ servermock.ResponseFromFixture("services.json")).
+ Build(t)
services, err := client.GetServices(t.Context())
require.NoError(t, err)
@@ -83,14 +65,21 @@ func TestClient_GetServices(t *testing.T) {
}
func TestClient_GetServices_errors(t *testing.T) {
- client := setupTest(t, "GET /v1/user/self/service", http.StatusUnauthorized, "error_v1.json")
+ client := mockBuilder().
+ Route("GET /v1/user/self/service",
+ servermock.ResponseFromFixture("error_v1.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetServices(t.Context())
require.EqualError(t, err, "401: No username or password.")
}
func TestClient_GetRecords(t *testing.T) {
- client := setupTest(t, "GET /v2/service/aaa/dns/record", http.StatusOK, "records.json")
+ client := mockBuilder().
+ Route("GET /v2/service/aaa/dns/record",
+ servermock.ResponseFromFixture("records.json")).
+ Build(t)
filter := RecordFilter{
Name: "example.com",
@@ -115,7 +104,11 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_errors(t *testing.T) {
- client := setupTest(t, "GET /v2/service/aaa/dns/record", http.StatusForbidden, "error_403.json")
+ client := mockBuilder().
+ Route("GET /v2/service/aaa/dns/record",
+ servermock.ResponseFromFixture("error_403.json").
+ WithStatusCode(http.StatusForbidden)).
+ Build(t)
filter := RecordFilter{
Name: "example.com",
@@ -128,28 +121,44 @@ func TestClient_GetRecords_errors(t *testing.T) {
}
func TestClient_CreateRecord(t *testing.T) {
- client := setupTest(t, "POST /v2/service/aaa/dns/record", http.StatusNoContent, "")
+ client := mockBuilder().
+ Route("POST /v2/service/aaa/dns/record",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.CreateRecord(t.Context(), "aaa", Record{})
require.NoError(t, err)
}
func TestClient_CreateRecord_errors(t *testing.T) {
- client := setupTest(t, "POST /v2/service/aaa/dns/record", http.StatusForbidden, "error_403.json")
+ client := mockBuilder().
+ Route("POST /v2/service/aaa/dns/record",
+ servermock.ResponseFromFixture("error_403.json").
+ WithStatusCode(http.StatusForbidden)).
+ Build(t)
err := client.CreateRecord(t.Context(), "aaa", Record{})
require.EqualError(t, err, "403: /errors/httpException: This action is unauthorized.")
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "DELETE /v2/service/aaa/dns/record/123", http.StatusNoContent, "")
+ client := mockBuilder().
+ Route("DELETE /v2/service/aaa/dns/record/123",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "aaa", "123")
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "DELETE /v2/service/aaa/dns/record/123", http.StatusForbidden, "error_403.json")
+ client := mockBuilder().
+ Route("DELETE /v2/service/aaa/dns/record/123",
+ servermock.ResponseFromFixture("error_403.json").
+ WithStatusCode(http.StatusForbidden)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "aaa", "123")
require.EqualError(t, err, "403: /errors/httpException: This action is unauthorized.")
diff --git a/providers/dns/internal/hostingde/client_test.go b/providers/dns/internal/hostingde/client_test.go
index c4090ec5c..b735509c0 100644
--- a/providers/dns/internal/hostingde/client_test.go
+++ b/providers/dns/internal/hostingde/client_test.go
@@ -1,69 +1,30 @@
package hostingde
import (
- "bytes"
"encoding/json"
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("secret")
client.HTTPClient = server.Client()
client.BaseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, handler)
-
- return client
-}
-
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
+ return client, nil
}
func TestClient_ListZoneConfigs(t *testing.T) {
- client := setupTest(t, "/zoneConfigsFind", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- raw, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- body := string(bytes.TrimSpace(raw))
- if body != `{"authToken":"secret","filter":{"field":"zoneName","value":"example.com"},"limit":1,"page":1}` {
- http.Error(rw, fmt.Sprintf("unexpected body: got %s", body), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, "zoneConfigsFind.json")
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /zoneConfigsFind",
+ servermock.ResponseFromFixture("zoneConfigsFind.json"),
+ servermock.CheckRequestJSONBodyFromFile("zoneConfigsFind-request.json")).
+ Build(t)
zonesFind := ZoneConfigsFindRequest{
Filter: Filter{Field: "zoneName", Value: "example.com"},
@@ -108,14 +69,10 @@ func TestClient_ListZoneConfigs(t *testing.T) {
}
func TestClient_ListZoneConfigs_error(t *testing.T) {
- client := setupTest(t, "/zoneConfigsFind", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, "zoneConfigsFind_error.json")
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /zoneConfigsFind",
+ servermock.ResponseFromFixture("zoneConfigsFind_error.json")).
+ Build(t)
zonesFind := ZoneConfigsFindRequest{
Filter: Filter{Field: "zoneName", Value: "example.com"},
@@ -128,26 +85,11 @@ func TestClient_ListZoneConfigs_error(t *testing.T) {
}
func TestClient_UpdateZone(t *testing.T) {
- client := setupTest(t, "/zoneUpdate", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- raw, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- body := string(bytes.TrimSpace(raw))
- if body != `{"authToken":"secret","zoneConfig":{"id":"123","accountId":"456","status":"s","name":"n","nameUnicode":"u","masterIp":"m","type":"t","emailAddress":"e","zoneTransferWhitelist":["a","b"],"lastChangeDate":"l","dnsServerGroupId":"g","dnsSecMode":"m","soaValues":{"refresh":1,"retry":2,"expire":3,"ttl":4,"negativeTtl":5}},"recordsToAdd":null,"recordsToDelete":[{"name":"_acme-challenge.example.com","type":"TXT","content":"\"txt\""}]}` {
- http.Error(rw, fmt.Sprintf("unexpected body: got %s", body), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, "zoneUpdate.json")
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /zoneUpdate",
+ servermock.ResponseFromFixture("zoneUpdate.json"),
+ servermock.CheckRequestJSONBodyFromFile("zoneUpdate-request.json")).
+ Build(t)
request := ZoneUpdateRequest{
ZoneConfig: ZoneConfig{
@@ -220,14 +162,10 @@ func TestClient_UpdateZone(t *testing.T) {
}
func TestClient_UpdateZone_error(t *testing.T) {
- client := setupTest(t, "/zoneUpdate", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, "zoneUpdate_error.json")
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /zoneUpdate",
+ servermock.ResponseFromFixture("zoneUpdate_error.json")).
+ Build(t)
request := ZoneUpdateRequest{
ZoneConfig: ZoneConfig{
diff --git a/providers/dns/internal/hostingde/fixtures/zoneConfigsFind-request.json b/providers/dns/internal/hostingde/fixtures/zoneConfigsFind-request.json
new file mode 100644
index 000000000..eb552d9eb
--- /dev/null
+++ b/providers/dns/internal/hostingde/fixtures/zoneConfigsFind-request.json
@@ -0,0 +1,9 @@
+{
+ "authToken": "secret",
+ "filter": {
+ "field": "zoneName",
+ "value": "example.com"
+ },
+ "limit": 1,
+ "page": 1
+}
diff --git a/providers/dns/internal/hostingde/fixtures/zoneUpdate-request.json b/providers/dns/internal/hostingde/fixtures/zoneUpdate-request.json
new file mode 100644
index 000000000..38b1be50d
--- /dev/null
+++ b/providers/dns/internal/hostingde/fixtures/zoneUpdate-request.json
@@ -0,0 +1,35 @@
+{
+ "authToken": "secret",
+ "zoneConfig": {
+ "id": "123",
+ "accountId": "456",
+ "status": "s",
+ "name": "n",
+ "nameUnicode": "u",
+ "masterIp": "m",
+ "type": "t",
+ "emailAddress": "e",
+ "zoneTransferWhitelist": [
+ "a",
+ "b"
+ ],
+ "lastChangeDate": "l",
+ "dnsServerGroupId": "g",
+ "dnsSecMode": "m",
+ "soaValues": {
+ "refresh": 1,
+ "retry": 2,
+ "expire": 3,
+ "ttl": 4,
+ "negativeTtl": 5
+ }
+ },
+ "recordsToAdd": null,
+ "recordsToDelete": [
+ {
+ "name": "_acme-challenge.example.com",
+ "type": "TXT",
+ "content": "\"txt\""
+ }
+ ]
+}
diff --git a/providers/dns/internal/rimuhosting/client_test.go b/providers/dns/internal/rimuhosting/client_test.go
index 90a574b3a..6ee9ea3f7 100644
--- a/providers/dns/internal/rimuhosting/client_test.go
+++ b/providers/dns/internal/rimuhosting/client_test.go
@@ -2,63 +2,42 @@ package rimuhosting
import (
"encoding/xml"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("apikeyvaluehere")
client.BaseURL = server.URL
client.HTTPClient = server.Client()
- return client, mux
+ return client, nil
}
func TestClient_FindTXTRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- query := req.URL.Query()
-
- var fixture string
- switch query.Get("name") {
- case "example.com":
- fixture = "./fixtures/find_records.xml"
- case "**.example.com":
- fixture = "./fixtures/find_records_pattern.xml"
- default:
- fixture = "./fixtures/find_records_empty.xml"
- }
-
- err := writeResponse(rw, fixture)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
testCases := []struct {
desc string
domain string
+ response string
+ query url.Values
expected []Record
}{
{
- desc: "simple",
- domain: "example.com",
+ desc: "simple",
+ domain: "example.com",
+ response: "find_records.xml",
+ query: url.Values{
+ "name": []string{"example.com"},
+ "type": []string{"TXT"},
+ "action": []string{"QUERY"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
expected: []Record{
{
Name: "example.org",
@@ -70,8 +49,15 @@ func TestClient_FindTXTRecords(t *testing.T) {
},
},
{
- desc: "pattern",
- domain: "**.example.com",
+ desc: "pattern",
+ domain: "**.example.com",
+ response: "find_records_pattern.xml",
+ query: url.Values{
+ "name": []string{"**.example.com"},
+ "type": []string{"TXT"},
+ "action": []string{"QUERY"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
expected: []Record{
{
Name: "_test.example.org",
@@ -92,12 +78,26 @@ func TestClient_FindTXTRecords(t *testing.T) {
{
desc: "empty",
domain: "empty.com",
+ response: "find_records_empty.xml",
+ query: url.Values{
+ "name": []string{"empty.com"},
+ "type": []string{"TXT"},
+ "action": []string{"QUERY"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
expected: nil,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /",
+ servermock.ResponseFromFixture(test.response),
+ servermock.CheckQueryParameter().Strict().
+ WithValues(test.query)).
+ Build(t)
+
records, err := client.FindTXTRecords(t.Context(), test.domain)
require.NoError(t, err)
@@ -107,54 +107,42 @@ func TestClient_FindTXTRecords(t *testing.T) {
}
func TestClient_DoActions(t *testing.T) {
- type expected struct {
- Query string
- Resp *DNSAPIResult
- Error string
- }
-
testCases := []struct {
desc string
actions []ActionParameter
- fixture string
- expected expected
+ query url.Values
+ response string
+ expected *DNSAPIResult
}{
- {
- desc: "SET error",
- actions: []ActionParameter{
- NewAddRecordAction("example.com", "txttxtx", 0),
- },
- fixture: "./fixtures/add_record_error.xml",
- expected: expected{
- Query: "action=SET&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx",
- Error: "ERROR: No zone found for example.com",
- },
- },
{
desc: "SET simple",
actions: []ActionParameter{
NewAddRecordAction("example.org", "txttxtx", 0),
},
- fixture: "./fixtures/add_record.xml",
- expected: expected{
- Query: "action=SET&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx",
- Resp: &DNSAPIResult{
- XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
- IsOk: "OK:",
- ResultCounts: ResultCounts{Added: "1", Changed: "0", Unchanged: "0", Deleted: "0"},
- Actions: Actions{
- Action: Action{
- Action: "SET",
- Host: "example.org",
- Type: "TXT",
- Records: []Record{{
- Name: "example.org",
- Type: "TXT",
- Content: "txttxtx",
- TTL: "3600 seconds",
- Priority: "0",
- }},
- },
+ response: "add_record.xml",
+ query: url.Values{
+ "action": []string{"SET"},
+ "name": []string{"example.org"},
+ "type": []string{"TXT"},
+ "value": []string{"txttxtx"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
+ expected: &DNSAPIResult{
+ XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
+ IsOk: "OK:",
+ ResultCounts: ResultCounts{Added: "1", Changed: "0", Unchanged: "0", Deleted: "0"},
+ Actions: Actions{
+ Action: Action{
+ Action: "SET",
+ Host: "example.org",
+ Type: "TXT",
+ Records: []Record{{
+ Name: "example.org",
+ Type: "TXT",
+ Content: "txttxtx",
+ TTL: "3600 seconds",
+ Priority: "0",
+ }},
},
},
},
@@ -165,69 +153,72 @@ func TestClient_DoActions(t *testing.T) {
NewAddRecordAction("example.org", "txttxtx", 0),
NewAddRecordAction("example.org", "sample", 0),
},
- fixture: "./fixtures/add_record_same_domain.xml",
- expected: expected{
- Query: "action[0]=SET&action[1]=SET&api_key=apikeyvaluehere&name[0]=example.org&name[1]=example.org&ttl[0]=0&ttl[1]=0&type[0]=TXT&type[1]=TXT&value[0]=txttxtx&value[1]=sample",
- Resp: &DNSAPIResult{
- XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
- IsOk: "OK:",
- ResultCounts: ResultCounts{Added: "2", Changed: "0", Unchanged: "0", Deleted: "0"},
- Actions: Actions{
- Action: Action{
- Action: "SET",
- Host: "example.org",
- Type: "TXT",
- Records: []Record{
- {
- Name: "example.org",
- Type: "TXT",
- Content: "txttxtx",
- TTL: "0 seconds",
- Priority: "0",
- },
- {
- Name: "example.org",
- Type: "TXT",
- Content: "sample",
- TTL: "0 seconds",
- Priority: "0",
- },
+ response: "add_record_same_domain.xml",
+ query: url.Values{
+ "api_key": []string{"apikeyvaluehere"},
+ "action[0]": []string{"SET"},
+ "name[0]": []string{"example.org"},
+ "ttl[0]": []string{"0"},
+ "type[0]": []string{"TXT"},
+ "value[0]": []string{"txttxtx"},
+ "action[1]": []string{"SET"},
+ "name[1]": []string{"example.org"},
+ "ttl[1]": []string{"0"},
+ "type[1]": []string{"TXT"},
+ "value[1]": []string{"sample"},
+ },
+ expected: &DNSAPIResult{
+ XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
+ IsOk: "OK:",
+ ResultCounts: ResultCounts{Added: "2", Changed: "0", Unchanged: "0", Deleted: "0"},
+ Actions: Actions{
+ Action: Action{
+ Action: "SET",
+ Host: "example.org",
+ Type: "TXT",
+ Records: []Record{
+ {
+ Name: "example.org",
+ Type: "TXT",
+ Content: "txttxtx",
+ TTL: "0 seconds",
+ Priority: "0",
+ },
+ {
+ Name: "example.org",
+ Type: "TXT",
+ Content: "sample",
+ TTL: "0 seconds",
+ Priority: "0",
},
},
},
},
},
},
- {
- desc: "DELETE error",
- actions: []ActionParameter{
- NewDeleteRecordAction("example.com", "txttxtx"),
- },
- fixture: "./fixtures/delete_record_error.xml",
- expected: expected{
- Query: "action=DELETE&api_key=apikeyvaluehere&name=example.com&type=TXT&value=txttxtx",
- Error: "ERROR: No zone found for example.com",
- },
- },
{
desc: "DELETE nothing",
actions: []ActionParameter{
NewDeleteRecordAction("example.org", "nothing"),
},
- fixture: "./fixtures/delete_record_nothing.xml",
- expected: expected{
- Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=nothing",
- Resp: &DNSAPIResult{
- XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
- IsOk: "OK:",
- ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "0"},
- Actions: Actions{
- Action: Action{
- Action: "DELETE",
- Host: "example.org",
- Type: "TXT",
- Records: nil,
- },
+ response: "delete_record_nothing.xml",
+ query: url.Values{
+ "action": []string{"DELETE"},
+ "name": []string{"example.org"},
+ "type": []string{"TXT"},
+ "value": []string{"nothing"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
+ expected: &DNSAPIResult{
+ XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
+ IsOk: "OK:",
+ ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "0"},
+ Actions: Actions{
+ Action: Action{
+ Action: "DELETE",
+ Host: "example.org",
+ Type: "TXT",
+ Records: nil,
},
},
},
@@ -237,26 +228,30 @@ func TestClient_DoActions(t *testing.T) {
actions: []ActionParameter{
NewDeleteRecordAction("example.org", "txttxtx"),
},
- fixture: "./fixtures/delete_record.xml",
- expected: expected{
- Query: "action=DELETE&api_key=apikeyvaluehere&name=example.org&type=TXT&value=txttxtx",
- Resp: &DNSAPIResult{
- XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
- IsOk: "OK:",
- ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "1"},
- Actions: Actions{
- Action: Action{
- Action: "DELETE",
- Host: "example.org",
- Type: "TXT",
- Records: []Record{{
- Name: "example.org",
- Type: "TXT",
- Content: "txttxtx",
- TTL: "3600 seconds",
- Priority: "0",
- }},
- },
+ response: "delete_record.xml",
+ query: url.Values{
+ "action": []string{"DELETE"},
+ "name": []string{"example.org"},
+ "type": []string{"TXT"},
+ "value": []string{"txttxtx"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
+ expected: &DNSAPIResult{
+ XMLName: xml.Name{Space: "", Local: "dnsapi_result"},
+ IsOk: "OK:",
+ ResultCounts: ResultCounts{Added: "0", Changed: "0", Unchanged: "0", Deleted: "1"},
+ Actions: Actions{
+ Action: Action{
+ Action: "DELETE",
+ Host: "example.org",
+ Type: "TXT",
+ Records: []Record{{
+ Name: "example.org",
+ Type: "TXT",
+ Content: "txttxtx",
+ TTL: "3600 seconds",
+ Priority: "0",
+ }},
},
},
},
@@ -265,52 +260,73 @@ func TestClient_DoActions(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- query, err := url.QueryUnescape(req.URL.RawQuery)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if test.expected.Query != query {
- http.Error(rw, fmt.Sprintf("invalid query: %s", query), http.StatusBadRequest)
- return
- }
-
- if test.expected.Error != "" {
- rw.WriteHeader(http.StatusInternalServerError)
- }
-
- err = writeResponse(rw, test.fixture)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /",
+ servermock.ResponseFromFixture(test.response),
+ servermock.CheckQueryParameter().Strict().
+ WithValues(test.query)).
+ Build(t)
resp, err := client.DoActions(t.Context(), test.actions...)
- if test.expected.Error != "" {
- require.EqualError(t, err, test.expected.Error)
- return
- }
-
require.NoError(t, err)
- assert.Equal(t, test.expected.Resp, resp)
+ assert.Equal(t, test.expected, resp)
})
}
}
-func writeResponse(rw io.Writer, filename string) error {
- file, err := os.Open(filename)
- if err != nil {
- return err
+func TestClient_DoActions_error(t *testing.T) {
+ testCases := []struct {
+ desc string
+ actions []ActionParameter
+ query url.Values
+ response string
+ expected string
+ }{
+ {
+ desc: "SET error",
+ actions: []ActionParameter{
+ NewAddRecordAction("example.com", "txttxtx", 0),
+ },
+ response: "add_record_error.xml",
+ query: url.Values{
+ "action": []string{"SET"},
+ "name": []string{"example.com"},
+ "type": []string{"TXT"},
+ "value": []string{"txttxtx"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
+ expected: "ERROR: No zone found for example.com",
+ },
+ {
+ desc: "DELETE error",
+ actions: []ActionParameter{
+ NewDeleteRecordAction("example.com", "txttxtx"),
+ },
+ response: "delete_record_error.xml",
+ query: url.Values{
+ "action": []string{"DELETE"},
+ "name": []string{"example.com"},
+ "type": []string{"TXT"},
+ "value": []string{"txttxtx"},
+ "api_key": []string{"apikeyvaluehere"},
+ },
+ expected: "ERROR: No zone found for example.com",
+ },
}
- defer func() { _ = file.Close() }()
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /",
+ servermock.ResponseFromFixture(test.response).
+ WithStatusCode(http.StatusInternalServerError),
+ servermock.CheckQueryParameter().Strict().
+ WithValues(test.query)).
+ Build(t)
- _, err = io.Copy(rw, file)
- return err
+ _, err := client.DoActions(t.Context(), test.actions...)
+ require.EqualError(t, err, test.expected)
+ })
+ }
}
diff --git a/providers/dns/internal/selectel/client_test.go b/providers/dns/internal/selectel/client_test.go
index d0a2f8cf0..d67a4b3bf 100644
--- a/providers/dns/internal/selectel/client_test.go
+++ b/providers/dns/internal/selectel/client_test.go
@@ -1,50 +1,29 @@
package selectel
import (
- "encoding/json"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("token")
client.BaseURL, _ = url.Parse(server.URL)
client.HTTPClient = server.Client()
- return client, mux
+ return client, nil
}
func TestClient_ListRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- fixture := "./fixtures/list_records.json"
-
- err := writeResponse(rw, fixture)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders()).
+ Route("GET /123/records/", servermock.ResponseFromFixture("list_records.json")).
+ Build(t)
records, err := client.ListRecords(t.Context(), 123)
require.NoError(t, err)
@@ -59,21 +38,12 @@ func TestClient_ListRecords(t *testing.T) {
}
func TestClient_ListRecords_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- rw.WriteHeader(http.StatusUnauthorized)
- err := writeResponse(rw, "./fixtures/error.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ With(tokenHeader, "token")).
+ Route("GET /123/records/",
+ servermock.ResponseFromFixture("error.json").WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
records, err := client.ListRecords(t.Context(), 123)
@@ -82,40 +52,16 @@ func TestClient_ListRecords_error(t *testing.T) {
}
func TestClient_GetDomainByName(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/sub.sub.example.org", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- rw.WriteHeader(http.StatusNotFound)
- })
-
- mux.HandleFunc("/sub.example.org", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- rw.WriteHeader(http.StatusNotFound)
- })
-
- mux.HandleFunc("/example.org", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- fixture := "./fixtures/domains.json"
-
- err := writeResponse(rw, fixture)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ With(tokenHeader, "token")).
+ Route("GET /sub.sub.example.org",
+ servermock.Noop().WithStatusCode(http.StatusNotFound)).
+ Route("GET /sub.example.org",
+ servermock.Noop().WithStatusCode(http.StatusNotFound)).
+ Route("GET /example.org",
+ servermock.ResponseFromFixture("domains.json")).
+ Build(t)
domain, err := client.GetDomainByName(t.Context(), "sub.sub.example.org")
require.NoError(t, err)
@@ -129,30 +75,13 @@ func TestClient_GetDomainByName(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/123/records/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- rec := Record{}
-
- err := json.NewDecoder(req.Body).Decode(&rec)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- rec.ID = 456
-
- err = json.NewEncoder(rw).Encode(rec)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ With(tokenHeader, "token")).
+ Route("POST /123/records/",
+ servermock.ResponseFromFixture("add_record.json"),
+ servermock.CheckRequestJSONBodyFromFile("add_record-request.json")).
+ Build(t)
record, err := client.AddRecord(t.Context(), 123, Record{
Name: "example.org",
@@ -177,27 +106,12 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ With(tokenHeader, "token")).
+ Route("DELETE /123/records/456", nil).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.NoError(t, err)
}
-
-func writeResponse(rw io.Writer, filename string) error {
- file, err := os.Open(filename)
- if err != nil {
- return err
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- return err
-}
diff --git a/providers/dns/internal/selectel/fixtures/add_record-request.json b/providers/dns/internal/selectel/fixtures/add_record-request.json
new file mode 100644
index 000000000..c65d3d267
--- /dev/null
+++ b/providers/dns/internal/selectel/fixtures/add_record-request.json
@@ -0,0 +1,7 @@
+{
+ "name": "example.org",
+ "type": "TXT",
+ "ttl": 60,
+ "email": "email@example.org",
+ "content": "txttxttxttxt"
+}
diff --git a/providers/dns/internal/selectel/fixtures/add_record.json b/providers/dns/internal/selectel/fixtures/add_record.json
new file mode 100644
index 000000000..18a436707
--- /dev/null
+++ b/providers/dns/internal/selectel/fixtures/add_record.json
@@ -0,0 +1,8 @@
+{
+ "id": 456,
+ "name": "example.org",
+ "type": "TXT",
+ "ttl": 60,
+ "email": "email@example.org",
+ "content": "txttxttxttxt"
+}
diff --git a/providers/dns/internetbs/internal/client_test.go b/providers/dns/internetbs/internal/client_test.go
index d0b94ad4e..4532426d5 100644
--- a/providers/dns/internetbs/internal/client_test.go
+++ b/providers/dns/internetbs/internal/client_test.go
@@ -2,14 +2,13 @@ package internal
import (
"fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -21,8 +20,33 @@ const (
testPassword = "testpass"
)
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(testAPIKey, testPassword)
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
+
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ )
+}
+
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/Domain/DnsRecord/Add", "./fixtures/Domain_DnsRecord_Add_SUCCESS.json")
+ client := mockBuilder().
+ Route("POST /Domain/DnsRecord/Add",
+ servermock.ResponseFromFixture("Domain_DnsRecord_Add_SUCCESS.json"),
+ servermock.CheckForm().Strict().
+ With("fullrecordname", "www.example.com").
+ With("ttl", "36000").
+ With("type", "TXT").
+ With("value", "xxx").
+ With("password", testPassword).
+ With("apiKey", testAPIKey).
+ With("ResponseFormat", "JSON")).
+ Build(t)
query := RecordQuery{
FullRecordName: "www.example.com",
@@ -36,7 +60,10 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "/Domain/DnsRecord/Add", "./fixtures/Domain_DnsRecord_Add_FAILURE.json")
+ client := mockBuilder().
+ Route("POST /Domain/DnsRecord/Add",
+ servermock.ResponseFromFixture("Domain_DnsRecord_Add_FAILURE.json")).
+ Build(t)
query := RecordQuery{
FullRecordName: "www.example.com.",
@@ -81,7 +108,16 @@ func TestClient_AddRecord_integration(t *testing.T) {
}
func TestClient_RemoveRecord(t *testing.T) {
- client := setupTest(t, "/Domain/DnsRecord/Remove", "./fixtures/Domain_DnsRecord_Remove_SUCCESS.json")
+ client := mockBuilder().
+ Route("POST /Domain/DnsRecord/Remove",
+ servermock.ResponseFromFixture("Domain_DnsRecord_Remove_SUCCESS.json"),
+ servermock.CheckForm().Strict().
+ With("fullrecordname", "www.example.com").
+ With("type", "TXT").
+ With("password", testPassword).
+ With("apiKey", testAPIKey).
+ With("ResponseFormat", "JSON")).
+ Build(t)
query := RecordQuery{
FullRecordName: "www.example.com",
@@ -93,7 +129,10 @@ func TestClient_RemoveRecord(t *testing.T) {
}
func TestClient_RemoveRecord_error(t *testing.T) {
- client := setupTest(t, "/Domain/DnsRecord/Remove", "./fixtures/Domain_DnsRecord_Remove_FAILURE.json")
+ client := mockBuilder().
+ Route("POST /Domain/DnsRecord/Remove",
+ servermock.ResponseFromFixture("Domain_DnsRecord_Remove_FAILURE.json")).
+ Build(t)
query := RecordQuery{
FullRecordName: "www.example.com.",
@@ -125,7 +164,15 @@ func TestClient_RemoveRecord_integration(t *testing.T) {
}
func TestClient_ListRecords(t *testing.T) {
- client := setupTest(t, "/Domain/DnsRecord/List", "./fixtures/Domain_DnsRecord_List_SUCCESS.json")
+ client := mockBuilder().
+ Route("POST /Domain/DnsRecord/List",
+ servermock.ResponseFromFixture("Domain_DnsRecord_List_SUCCESS.json"),
+ servermock.CheckForm().Strict().
+ With("Domain", "example.com").
+ With("password", testPassword).
+ With("apiKey", testAPIKey).
+ With("ResponseFormat", "JSON")).
+ Build(t)
query := ListRecordQuery{
Domain: "example.com",
@@ -177,7 +224,10 @@ func TestClient_ListRecords(t *testing.T) {
}
func TestClient_ListRecords_error(t *testing.T) {
- client := setupTest(t, "/Domain/DnsRecord/List", "./fixtures/Domain_DnsRecord_List_FAILURE.json")
+ client := mockBuilder().
+ Route("POST /Domain/DnsRecord/List",
+ servermock.ResponseFromFixture("Domain_DnsRecord_List_FAILURE.json")).
+ Build(t)
query := ListRecordQuery{
Domain: "www.example.com",
@@ -208,51 +258,3 @@ func TestClient_ListRecords_integration(t *testing.T) {
fmt.Println(record)
}
}
-
-func setupTest(t *testing.T, path, filename string) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(path, testHandler(filename))
-
- client := NewClient(testAPIKey, testPassword)
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
-}
-
-func testHandler(filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- if req.FormValue("apiKey") != testAPIKey {
- http.Error(rw, `{"transactid":"d46d812569acdb8b39c3933ec4351e79","status":"FAILURE","message":"Invalid API key and\/or Password","code":107002}`, http.StatusOK)
- return
- }
-
- if req.FormValue("password") != testPassword {
- http.Error(rw, `{"transactid":"d46d812569acdb8b39c3933ec4351e79","status":"FAILURE","message":"Invalid API key and\/or Password","code":107002}`, http.StatusOK)
- return
- }
-
- file, err := os.Open(filename)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
diff --git a/providers/dns/internetbs/internal/fixtures/auth_error.json b/providers/dns/internetbs/internal/fixtures/auth_error.json
new file mode 100644
index 000000000..a40a0ef5e
--- /dev/null
+++ b/providers/dns/internetbs/internal/fixtures/auth_error.json
@@ -0,0 +1,6 @@
+{
+ "transactid": "d46d812569acdb8b39c3933ec4351e79",
+ "status": "FAILURE",
+ "message": "Invalid API key and\/or Password",
+ "code": 107002
+}
diff --git a/providers/dns/ionos/internal/client.go b/providers/dns/ionos/internal/client.go
index 8b37d5f1c..b51e003f7 100644
--- a/providers/dns/ionos/internal/client.go
+++ b/providers/dns/ionos/internal/client.go
@@ -17,6 +17,9 @@ import (
// defaultBaseURL represents the API endpoint to call.
const defaultBaseURL = "https://api.hosting.ionos.com/dns"
+// APIKeyHeader API key header.
+const APIKeyHeader = "X-Api-Key"
+
// Client Ionos API client.
type Client struct {
apiKey string
@@ -119,7 +122,7 @@ func (c *Client) RemoveRecord(ctx context.Context, zoneID, recordID string) erro
}
func (c *Client) do(req *http.Request, result any) error {
- req.Header.Set("X-API-Key", c.apiKey)
+ req.Header.Set(APIKeyHeader, c.apiKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
diff --git a/providers/dns/ionos/internal/client_test.go b/providers/dns/ionos/internal/client_test.go
index 6a36dfde7..008d153bc 100644
--- a/providers/dns/ionos/internal/client_test.go
+++ b/providers/dns/ionos/internal/client_test.go
@@ -1,24 +1,38 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestClient_ListZones(t *testing.T) {
- client, mux := setupTest(t)
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("secret")
+ if err != nil {
+ return nil, err
+ }
- mux.HandleFunc("/v1/zones", mockHandler(http.MethodGet, http.StatusOK, "list_zones.json"))
+ client.BaseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ servermock.CheckHeader().With(APIKeyHeader, "secret"))
+}
+
+func TestClient_ListZones(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /v1/zones",
+ servermock.ResponseFromFixture("list_zones.json")).
+ Build(t)
zones, err := client.ListZones(t.Context())
require.NoError(t, err)
@@ -33,9 +47,11 @@ func TestClient_ListZones(t *testing.T) {
}
func TestClient_ListZones_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/zones", mockHandler(http.MethodGet, http.StatusUnauthorized, "list_zones_error.json"))
+ client := mockBuilder().
+ Route("GET /v1/zones",
+ servermock.ResponseFromFixture("list_zones_error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
zones, err := client.ListZones(t.Context())
require.Error(t, err)
@@ -48,9 +64,10 @@ func TestClient_ListZones_error(t *testing.T) {
}
func TestClient_GetRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodGet, http.StatusOK, "get_records.json"))
+ client := mockBuilder().
+ Route("GET /v1/zones/azone01",
+ servermock.ResponseFromFixture("get_records.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "azone01", nil)
require.NoError(t, err)
@@ -66,9 +83,11 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodGet, http.StatusUnauthorized, "get_records_error.json"))
+ client := mockBuilder().
+ Route("GET /v1/zones/azone01",
+ servermock.ResponseFromFixture("get_records_error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
records, err := client.GetRecords(t.Context(), "azone01", nil)
require.Error(t, err)
@@ -81,18 +100,20 @@ func TestClient_GetRecords_error(t *testing.T) {
}
func TestClient_RemoveRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/zones/azone01/records/arecord01", mockHandler(http.MethodDelete, http.StatusOK, ""))
+ client := mockBuilder().
+ Route("DELETE /v1/zones/azone01/records/arecord01", nil).
+ Build(t)
err := client.RemoveRecord(t.Context(), "azone01", "arecord01")
require.NoError(t, err)
}
func TestClient_RemoveRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/zones/azone01/records/arecord01", mockHandler(http.MethodDelete, http.StatusInternalServerError, "remove_record_error.json"))
+ client := mockBuilder().
+ Route("DELETE /v1/zones/azone01/records/arecord01",
+ servermock.ResponseFromFixture("remove_record_error.json").
+ WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
err := client.RemoveRecord(t.Context(), "azone01", "arecord01")
require.Error(t, err)
@@ -103,9 +124,9 @@ func TestClient_RemoveRecord_error(t *testing.T) {
}
func TestClient_ReplaceRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodPatch, http.StatusOK, ""))
+ client := mockBuilder().
+ Route("PATCH /v1/zones/azone01", nil).
+ Build(t)
records := []Record{{
ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4",
@@ -119,9 +140,11 @@ func TestClient_ReplaceRecords(t *testing.T) {
}
func TestClient_ReplaceRecords_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v1/zones/azone01", mockHandler(http.MethodPatch, http.StatusBadRequest, "replace_records_error.json"))
+ client := mockBuilder().
+ Route("PATCH /v1/zones/azone01",
+ servermock.ResponseFromFixture("replace_records_error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
records := []Record{{
ID: "22af3414-abbe-9e11-5df5-66fbe8e334b4",
@@ -137,47 +160,3 @@ func TestClient_ReplaceRecords_error(t *testing.T) {
assert.ErrorAs(t, err, &cErr)
assert.Equal(t, http.StatusBadRequest, cErr.StatusCode)
}
-
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client, err := NewClient("secret")
- require.NoError(t, err)
-
- client.BaseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func mockHandler(method string, statusCode int, filename string) func(http.ResponseWriter, *http.Request) {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- if filename == "" {
- rw.WriteHeader(statusCode)
- return
- }
-
- file, err := os.Open(filepath.FromSlash(path.Join("./fixtures", filename)))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
diff --git a/providers/dns/ipv64/internal/client_test.go b/providers/dns/ipv64/internal/client_test.go
index 8f97d8ff2..ba5ede9fc 100644
--- a/providers/dns/ipv64/internal/client_test.go
+++ b/providers/dns/ipv64/internal/client_test.go
@@ -1,66 +1,33 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testAPIKey = "secret"
-func setupTest(t *testing.T, handler http.HandlerFunc) *Client {
- t.Helper()
-
- server := httptest.NewServer(handler)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient(OAuthStaticAccessToken(server.Client(), testAPIKey))
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
- return client
-}
-
-func testHandler(method, filename string, statusCode int) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Bearer "+testAPIKey {
- http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
+ return client, nil
}
func TestClient_GetDomains(t *testing.T) {
- client := setupTest(t, testHandler(http.MethodGet, "get_domains.json", http.StatusOK))
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /api",
+ servermock.ResponseFromFixture("get_domains.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("get_domains", "")).
+ Build(t)
domains, err := client.GetDomains(t.Context())
require.NoError(t, err)
@@ -111,7 +78,11 @@ func TestClient_GetDomains(t *testing.T) {
}
func TestClient_GetDomains_error(t *testing.T) {
- client := setupTest(t, testHandler(http.MethodGet, "error.json", http.StatusUnauthorized))
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /api",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
domains, err := client.GetDomains(t.Context())
require.Error(t, err)
@@ -120,28 +91,53 @@ func TestClient_GetDomains_error(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, testHandler(http.MethodPost, "add_record.json", http.StatusCreated))
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithContentTypeFromURLEncoded()).
+ Route("POST /api",
+ servermock.ResponseFromFixture("add_record.json").
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckForm().Strict().
+ With("add_record", "lego.ipv64.net").
+ With("content", "value").
+ With("praefix", "_acme-challenge").
+ With("type", "TXT"),
+ ).
+ Build(t)
err := client.AddRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value")
require.NoError(t, err)
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, testHandler(http.MethodPost, "add_record-error.json", http.StatusBadRequest))
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /api",
+ servermock.ResponseFromFixture("add_record-error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
err := client.AddRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value")
require.Error(t, err)
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, testHandler(http.MethodDelete, "del_record.json", http.StatusAccepted))
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithContentTypeFromURLEncoded()).
+ Route("DELETE /api",
+ // the query parameters can be checked because the Go server ignores the body of a DELETE request.
+ servermock.ResponseFromFixture("del_record.json").
+ WithStatusCode(http.StatusAccepted)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value")
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, testHandler(http.MethodDelete, "del_record-error.json", http.StatusBadRequest))
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("DELETE /api",
+ servermock.ResponseFromFixture("del_record-error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "lego.ipv64.net", "_acme-challenge", "TXT", "value")
require.Error(t, err)
diff --git a/providers/dns/iwantmyname/internal/client_test.go b/providers/dns/iwantmyname/internal/client_test.go
index 39dca6dca..c25eb56ef 100644
--- a/providers/dns/iwantmyname/internal/client_test.go
+++ b/providers/dns/iwantmyname/internal/client_test.go
@@ -7,72 +7,32 @@ import (
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func checkParameter(query url.Values, key, expected string) error {
- if query.Get(key) != expected {
- return fmt.Errorf("%s: want %s got %s", key, expected, query.Get(key))
- }
- return nil
-}
-
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("user", "secret")
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
- return client, mux
+ return client, nil
}
func TestClient_Do(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- username, password, ok := req.BasicAuth()
- if !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if username != "user" {
- http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "user"), http.StatusUnauthorized)
- return
- }
-
- if password != "secret" {
- http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized)
- return
- }
-
- query := req.URL.Query()
-
- values := map[string]string{
- "hostname": "example.com",
- "type": "TXT",
- "value": "data",
- "ttl": "120",
- }
-
- for k, v := range values {
- err := checkParameter(query, k, v)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().
+ WithBasicAuth("user", "secret"),
+ ).
+ Route("POST /", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ fmt.Println(req)
+ }),
+ servermock.CheckQueryParameter().Strict().
+ With("hostname", "example.com").
+ With("ttl", "120").
+ With("type", "TXT").
+ With("value", "data")).
+ Build(t)
record := Record{
Hostname: "example.com",
diff --git a/providers/dns/joker/internal/dmapi/client_test.go b/providers/dns/joker/internal/dmapi/client_test.go
index b7a294e09..5b6d68740 100644
--- a/providers/dns/joker/internal/dmapi/client_test.go
+++ b/providers/dns/joker/internal/dmapi/client_test.go
@@ -7,6 +7,7 @@ import (
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -23,14 +24,17 @@ const (
serverErrorUsername = "error"
)
-func setupTest(t *testing.T) (*http.ServeMux, string) {
- t.Helper()
+func mockBuilder(auth AuthInfo) *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(auth)
+ client.BaseURL = server.URL
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- return mux, server.URL
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded())
}
func TestClient_GetZone(t *testing.T) {
@@ -70,29 +74,24 @@ func TestClient_GetZone(t *testing.T) {
},
}
- mux, serverURL := setupTest(t)
+ client := mockBuilder(AuthInfo{APIKey: "12345"}).
+ Route("POST /dns-zone-get", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ authSid := req.FormValue("auth-sid")
+ domain := req.FormValue("domain")
- 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)
- }
- })
+ switch {
+ case authSid == correctAPIKey && domain == "known":
+ _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\n\n"+testZone)
+ case authSid == incorrectAPIKey || (authSid == correctAPIKey && domain == "unknown"):
+ _, _ = io.WriteString(rw, "Status-Code: 2202\nStatus-Text: Authorization error")
+ default:
+ http.NotFound(rw, req)
+ }
+ })).
+ Build(t)
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := NewClient(AuthInfo{APIKey: "12345"})
- client.BaseURL = serverURL
-
response, err := client.GetZone(mockContext(t, test.authSid), test.domain)
if test.expectedError {
require.Error(t, err)
diff --git a/providers/dns/joker/internal/dmapi/identity_test.go b/providers/dns/joker/internal/dmapi/identity_test.go
index b84321096..d2a80f2e6 100644
--- a/providers/dns/joker/internal/dmapi/identity_test.go
+++ b/providers/dns/joker/internal/dmapi/identity_test.go
@@ -5,7 +5,6 @@ import (
"fmt"
"io"
"net/http"
- "net/http/httptest"
"sync/atomic"
"testing"
"time"
@@ -58,27 +57,22 @@ func TestClient_login_apikey(t *testing.T) {
},
}
- mux, serverURL := setupTest(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
+ client := mockBuilder(AuthInfo{APIKey: test.apiKey}).
+ Route("POST /login", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ switch req.FormValue("api-key") {
+ case correctAPIKey:
+ _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet")
+ case incorrectAPIKey:
+ _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error")
+ case serverErrorAPIKey:
+ http.NotFound(rw, req)
+ default:
+ _, _ = io.WriteString(rw, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet")
+ }
+ })).
+ Build(t)
response, err := client.login(t.Context())
if test.expectedError {
@@ -133,27 +127,22 @@ func TestClient_login_username(t *testing.T) {
},
}
- mux, serverURL := setupTest(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
+ client := mockBuilder(AuthInfo{Username: test.username, Password: test.password}).
+ Route("POST /login", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ switch req.FormValue("username") {
+ case correctUsername:
+ _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: 123\n\ncom\nnet")
+ case incorrectUsername:
+ _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error")
+ case serverErrorUsername:
+ http.NotFound(rw, req)
+ default:
+ _, _ = io.WriteString(rw, "Status-Code: 2202\nStatus-Text: OK\n\ncom\nnet")
+ }
+ })).
+ Build(t)
response, err := client.login(t.Context())
if test.expectedError {
@@ -197,25 +186,21 @@ func TestClient_logout(t *testing.T) {
},
}
- mux, serverURL := setupTest(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"})
- client.BaseURL = serverURL
+ client := mockBuilder(AuthInfo{APIKey: "12345"}).
+ Route("POST /logout", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ switch req.FormValue("auth-sid") {
+ case correctAPIKey:
+ _, _ = io.WriteString(rw, "Status-Code: 0\nStatus-Text: OK\n")
+ case incorrectAPIKey:
+ _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error")
+ default:
+ http.NotFound(rw, req)
+ }
+ })).
+ Build(t)
+
client.token = &Token{SessionID: test.authSid}
response, err := client.Logout(mockContext(t, test.authSid))
@@ -231,29 +216,21 @@ func TestClient_logout(t *testing.T) {
}
func TestClient_CreateAuthenticatedContext(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
id := atomic.Int32{}
id.Add(100)
- mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodPost, r.Method)
+ client := mockBuilder(AuthInfo{Username: correctUsername, Password: "secret"}).
+ Route("POST /login", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ switch req.FormValue("username") {
+ case correctUsername:
+ _, _ = fmt.Fprintf(rw, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: %d\n\ncom\nnet", id.Load())
+ id.Add(100)
- switch r.FormValue("username") {
- case correctUsername:
- _, _ = fmt.Fprintf(w, "Status-Code: 0\nStatus-Text: OK\nAuth-Sid: %d\n\ncom\nnet", id.Load())
- id.Add(100)
-
- default:
- _, _ = io.WriteString(w, "Status-Code: 2200\nStatus-Text: Authentication error")
- }
- })
-
- client := NewClient(AuthInfo{Username: correctUsername, Password: "secret"})
- client.HTTPClient = server.Client()
- client.BaseURL = server.URL
+ default:
+ _, _ = io.WriteString(rw, "Status-Code: 2200\nStatus-Text: Authentication error")
+ }
+ })).
+ Build(t)
ctx, err := client.CreateAuthenticatedContext(t.Context())
require.NoError(t, err)
diff --git a/providers/dns/joker/internal/svc/client_test.go b/providers/dns/joker/internal/svc/client_test.go
index a396f67e5..a6cb299e4 100644
--- a/providers/dns/joker/internal/svc/client_test.go
+++ b/providers/dns/joker/internal/svc/client_test.go
@@ -1,51 +1,39 @@
package svc
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("test", "secret")
+ client.BaseURL = server.URL
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("test", "secret")
- client.BaseURL = server.URL
- client.HTTPClient = server.Client()
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded())
}
func TestClient_Send(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- all, _ := io.ReadAll(req.Body)
-
- if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=123&zone=example.com" {
- http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest)
- return
- }
-
- _, err := rw.Write([]byte("OK: 1 inserted, 0 deleted"))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /",
+ servermock.RawStringResponse("OK: 1 inserted, 0 deleted"),
+ servermock.CheckForm().Strict().
+ With("zone", "example.com").
+ With("label", "_acme-challenge").
+ With("type", "TXT").
+ With("value", "123").
+ With("username", "test").
+ With("password", "secret"),
+ ).
+ Build(t)
zone := "example.com"
label := "_acme-challenge"
@@ -56,27 +44,18 @@ func TestClient_Send(t *testing.T) {
}
func TestClient_Send_empty(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- all, _ := io.ReadAll(req.Body)
-
- if string(all) != "label=_acme-challenge&password=secret&type=TXT&username=test&value=&zone=example.com" {
- http.Error(rw, fmt.Sprintf("invalid request: %q", string(all)), http.StatusBadRequest)
- return
- }
-
- _, err := rw.Write([]byte("OK: 1 inserted, 0 deleted"))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /",
+ servermock.RawStringResponse("OK: 1 inserted, 0 deleted"),
+ servermock.CheckForm().Strict().
+ With("zone", "example.com").
+ With("label", "_acme-challenge").
+ With("type", "TXT").
+ With("value", "").
+ With("username", "test").
+ With("password", "secret"),
+ ).
+ Build(t)
zone := "example.com"
label := "_acme-challenge"
diff --git a/providers/dns/liara/internal/client_test.go b/providers/dns/liara/internal/client_test.go
index 233a4bc2b..57ac7e8b3 100644
--- a/providers/dns/liara/internal/client_test.go
+++ b/providers/dns/liara/internal/client_test.go
@@ -1,25 +1,34 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const apiKey = "key"
-func TestClient_GetRecords(t *testing.T) {
- client, mux := setupTest(t)
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey))
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc("/api/v1/zones/example.com/dns-records", testHandler("./RecordsResponse.json", http.MethodGet, http.StatusOK))
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer "+apiKey))
+}
+
+func TestClient_GetRecords(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /api/v1/zones/example.com/dns-records", servermock.ResponseFromFixture("RecordsResponse.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "example.com")
require.NoError(t, err)
@@ -41,9 +50,9 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", testHandler("./RecordResponse.json", http.MethodGet, http.StatusOK))
+ client := mockBuilder().
+ Route("GET /api/v1/zones/example.com/dns-records/123", servermock.ResponseFromFixture("RecordResponse.json")).
+ Build(t)
record, err := client.GetRecord(t.Context(), "example.com", "123")
require.NoError(t, err)
@@ -63,9 +72,12 @@ func TestClient_GetRecord(t *testing.T) {
}
func TestClient_CreateRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/api/v1/zones/example.com/dns-records", testHandler("./RecordResponse.json", http.MethodPost, http.StatusCreated))
+ client := mockBuilder().
+ Route("POST /api/v1/zones/example.com/dns-records",
+ servermock.ResponseFromFixture("RecordResponse.json").
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBody(`{"name":"string","type":"string","ttl":3600,"contents":[{"text":"string"}]}`)).
+ Build(t)
data := Record{
Type: "string",
@@ -97,76 +109,34 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", func(rw http.ResponseWriter, req *http.Request) {
- rw.WriteHeader(http.StatusNoContent)
- })
+ client := mockBuilder().
+ Route("DELETE /api/v1/zones/example.com/dns-records/123",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", "123")
require.NoError(t, err)
}
func TestClient_DeleteRecord_NotFound_Response(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", func(rw http.ResponseWriter, req *http.Request) {
- rw.WriteHeader(http.StatusNotFound)
- })
+ client := mockBuilder().
+ Route("DELETE /api/v1/zones/example.com/dns-records/123",
+ servermock.Noop().
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", "123")
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/api/v1/zones/example.com/dns-records/123", testHandler("./error.json", http.MethodDelete, http.StatusUnauthorized))
+ client := mockBuilder().
+ Route("DELETE /api/v1/zones/example.com/dns-records/123",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", "123")
- require.Error(t, err)
-}
-
-func testHandler(filename, method string, statusCode int) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Bearer "+apiKey {
- http.Error(rw, "invalid Authorization header", http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
-
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(OAuthStaticAccessToken(server.Client(), apiKey))
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ require.EqualError(t, err, "[status code: 401] Unauthorized: Invalid token missing header")
}
diff --git a/providers/dns/lightsail/lightsail_test.go b/providers/dns/lightsail/lightsail_test.go
index 4a11f6eb4..010e794a9 100644
--- a/providers/dns/lightsail/lightsail_test.go
+++ b/providers/dns/lightsail/lightsail_test.go
@@ -1,6 +1,7 @@
package lightsail
import (
+ "net/http/httptest"
"os"
"testing"
@@ -9,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/lightsail"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -30,20 +32,6 @@ var envTest = tester.NewEnvTest(
WithDomain(EnvDNSZone).
WithLiveTestRequirements(envAwsAccessKeyID, envAwsSecretAccessKey, EnvDNSZone)
-func makeProvider(serverURL string) *DNSProvider {
- config := aws.Config{
- Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "),
- Region: "mock-region",
- BaseEndpoint: aws.String(serverURL),
- RetryMaxAttempts: 1,
- }
-
- return &DNSProvider{
- client: lightsail.NewFromConfig(config),
- config: NewDefaultConfig(),
- }
-}
-
func TestCredentialsFromEnv(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
@@ -68,13 +56,20 @@ func TestCredentialsFromEnv(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- mockResponses := map[string]MockResponse{
- "/": {StatusCode: 200, Body: ""},
- }
-
- serverURL := newMockServer(t, mockResponses)
-
- provider := makeProvider(serverURL)
+ provider := servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ return &DNSProvider{
+ client: lightsail.NewFromConfig(aws.Config{
+ Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "),
+ Region: "mock-region",
+ BaseEndpoint: aws.String(server.URL),
+ RetryMaxAttempts: 1,
+ }),
+ config: NewDefaultConfig(),
+ }, nil
+ }).
+ Route("POST /", nil).
+ Build(t)
domain := "example.com"
keyAuth := "123456d=="
diff --git a/providers/dns/lightsail/mock_server_test.go b/providers/dns/lightsail/mock_server_test.go
deleted file mode 100644
index 385c80850..000000000
--- a/providers/dns/lightsail/mock_server_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package lightsail
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-
- "github.com/stretchr/testify/require"
-)
-
-// MockResponse represents a predefined response used by a mock server.
-type MockResponse struct {
- StatusCode int
- Body string
-}
-
-func newMockServer(t *testing.T, responses map[string]MockResponse) string {
- t.Helper()
-
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- path := r.URL.Path
- resp, ok := responses[path]
- if !ok {
- msg := fmt.Sprintf("Requested path not found in response map: %s", path)
- require.FailNow(t, msg)
- }
-
- w.Header().Set("Content-Type", "application/xml")
- w.WriteHeader(resp.StatusCode)
- _, err := w.Write([]byte(resp.Body))
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }))
-
- t.Cleanup(server.Close)
-
- time.Sleep(100 * time.Millisecond)
-
- return server.URL
-}
diff --git a/providers/dns/limacity/internal/client_test.go b/providers/dns/limacity/internal/client_test.go
index 307783953..c43f12ba2 100644
--- a/providers/dns/limacity/internal/client_test.go
+++ b/providers/dns/limacity/internal/client_test.go
@@ -1,69 +1,36 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const apiKey = "secret"
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(apiKey)
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(apiKey)
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func testHandler(filename, method string, statusCode int) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- username, key, ok := req.BasicAuth()
- if username != "api" || key != apiKey || !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("api", apiKey),
+ )
}
func TestClient_GetDomains(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains.json", testHandler("get-domains.json", http.MethodGet, http.StatusOK))
+ client := mockBuilder().
+ Route("GET /domains.json", servermock.ResponseFromFixture("get-domains.json")).
+ Build(t)
domains, err := client.GetDomains(t.Context())
require.NoError(t, err)
@@ -79,18 +46,20 @@ func TestClient_GetDomains(t *testing.T) {
}
func TestClient_GetDomains_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains.json", testHandler("error.json", http.MethodGet, http.StatusBadRequest))
+ client := mockBuilder().
+ Route("GET /domains.json",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
_, err := client.GetDomains(t.Context())
require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]")
}
func TestClient_GetRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records.json", testHandler("get-records.json", http.MethodGet, http.StatusOK))
+ client := mockBuilder().
+ Route("GET /domains/123/records.json", servermock.ResponseFromFixture("get-records.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), 123)
require.NoError(t, err)
@@ -115,18 +84,22 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records.json", testHandler("error.json", http.MethodGet, http.StatusBadRequest))
+ client := mockBuilder().
+ Route("GET /domains/123/records.json",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
_, err := client.GetRecords(t.Context(), 123)
require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]")
}
func TestClient_AddRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records.json", testHandler("ok.json", http.MethodPost, http.StatusOK))
+ client := mockBuilder().
+ Route("POST /domains/123/records.json",
+ servermock.ResponseFromFixture("ok.json"),
+ servermock.CheckRequestJSONBody(`{"nameserver_record":{"name":"foo","content":"bar","ttl":12,"type":"TXT"}}`)).
+ Build(t)
record := Record{
Name: "foo",
@@ -140,9 +113,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records.json", testHandler("error.json", http.MethodPost, http.StatusBadRequest))
+ client := mockBuilder().
+ Route("POST /domains/123/records.json",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
record := Record{
Name: "foo",
@@ -156,36 +131,43 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_UpdateRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records/456", testHandler("ok.json", http.MethodPut, http.StatusOK))
+ client := mockBuilder().
+ Route("PUT /domains/123/records/456",
+ servermock.ResponseFromFixture("ok.json"),
+ servermock.CheckRequestJSONBody(`{"nameserver_record":{}}`)).
+ Build(t)
err := client.UpdateRecord(t.Context(), 123, 456, Record{})
require.NoError(t, err)
}
func TestClient_UpdateRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records/456", testHandler("error.json", http.MethodPut, http.StatusBadRequest))
+ client := mockBuilder().
+ Route("PUT /domains/123/records/456",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
err := client.UpdateRecord(t.Context(), 123, 456, Record{})
require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]")
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records/456", testHandler("ok.json", http.MethodDelete, http.StatusOK))
+ client := mockBuilder().
+ Route("DELETE /domains/123/records/456",
+ servermock.ResponseFromFixture("ok.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/123/records/456", testHandler("error.json", http.MethodDelete, http.StatusBadRequest))
+ client := mockBuilder().
+ Route("DELETE /domains/123/records/456",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.EqualError(t, err, "[status code: 400] status: invalid_resource, details: name: [muss ausgefüllt werden]")
diff --git a/providers/dns/linode/linode_test.go b/providers/dns/linode/linode_test.go
index a6b8041f8..08549ab7e 100644
--- a/providers/dns/linode/linode_test.go
+++ b/providers/dns/linode/linode_test.go
@@ -1,69 +1,20 @@
package linode
import (
- "encoding/json"
- "fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
- "time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/linode/linodego"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-type MockResponseMap map[string]any
-
var envTest = tester.NewEnvTest(EnvToken)
-func setupTest(t *testing.T, responses MockResponseMap) string {
- t.Helper()
-
- handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Ensure that we support the requested action.
- action := r.Method + ":" + r.URL.Path
- resp, ok := responses[action]
- if !ok {
- http.Error(w, fmt.Sprintf("Unsupported mock action: %q", action), http.StatusInternalServerError)
- return
- }
-
- rawResponse, err := json.Marshal(resp)
- if err != nil {
- http.Error(w, fmt.Sprintf("Failed to JSON encode response: %v", err), http.StatusInternalServerError)
- return
- }
-
- // Send the response.
- w.Header().Set("Content-Type", "application/json")
- if err, ok := resp.(linodego.APIError); ok {
- if err.Errors[0].Reason == "Not found" {
- w.WriteHeader(http.StatusNotFound)
- } else {
- w.WriteHeader(http.StatusBadRequest)
- }
- } else {
- w.WriteHeader(http.StatusOK)
- }
-
- _, err = w.Write(rawResponse)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- time.Sleep(100 * time.Millisecond)
-
- return server.URL
-}
-
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
@@ -145,81 +96,77 @@ func TestDNSProvider_Present(t *testing.T) {
defer envTest.RestoreEnv()
os.Setenv(EnvToken, "testing")
- p, err := NewDNSProvider()
- require.NoError(t, err)
- require.NotNil(t, p)
-
domain := "example.com"
keyAuth := "dGVzdGluZw=="
testCases := []struct {
desc string
- mockResponses MockResponseMap
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
desc: "Success",
- mockResponses: MockResponseMap{
- "GET:/v4/domains": linodego.DomainsPagedResponse{
- PageOptions: &linodego.PageOptions{
- Pages: 1,
- Results: 1,
- Page: 1,
- },
- Data: []linodego.Domain{{
- Domain: domain,
- ID: 1234,
- }},
- },
- "POST:/v4/domains/1234/records": linodego.DomainRecord{
+ builder: mockBuilder().
+ Route("GET /v4/domains",
+ servermock.JSONEncode(linodego.DomainsPagedResponse{
+ PageOptions: &linodego.PageOptions{
+ Pages: 1,
+ Results: 1,
+ Page: 1,
+ },
+ Data: []linodego.Domain{{
+ Domain: domain,
+ ID: 1234,
+ }},
+ })).
+ Route("POST /v4/domains/1234/records", servermock.JSONEncode(linodego.DomainRecord{
ID: 1234,
- },
- },
+ })),
},
{
desc: "NoDomain",
- mockResponses: MockResponseMap{
- "GET:/v4/domains": linodego.APIError{
- Errors: []linodego.APIErrorReason{{
- Reason: "Not found",
- }},
- },
- },
+ builder: mockBuilder().
+ Route("GET /v4/domains",
+ servermock.JSONEncode(linodego.APIError{
+ Errors: []linodego.APIErrorReason{{
+ Reason: "Not found",
+ }},
+ }).
+ WithStatusCode(http.StatusNotFound)),
expectedError: "[404] Not found",
},
{
desc: "CreateFailed",
- mockResponses: MockResponseMap{
- "GET:/v4/domains": &linodego.DomainsPagedResponse{
- PageOptions: &linodego.PageOptions{
- Pages: 1,
- Results: 1,
- Page: 1,
- },
- Data: []linodego.Domain{{
- Domain: "example.com",
- ID: 1234,
- }},
- },
- "POST:/v4/domains/1234/records": linodego.APIError{
- Errors: []linodego.APIErrorReason{{
- Reason: "Failed to create domain resource",
- Field: "somefield",
- }},
- },
- },
+ builder: mockBuilder().
+ Route("GET /v4/domains",
+ servermock.JSONEncode(&linodego.DomainsPagedResponse{
+ PageOptions: &linodego.PageOptions{
+ Pages: 1,
+ Results: 1,
+ Page: 1,
+ },
+ Data: []linodego.Domain{{
+ Domain: "example.com",
+ ID: 1234,
+ }},
+ })).
+ Route("POST /v4/domains/1234/records",
+ servermock.JSONEncode(linodego.APIError{
+ Errors: []linodego.APIErrorReason{{
+ Reason: "Failed to create domain resource",
+ Field: "somefield",
+ }},
+ }).
+ WithStatusCode(http.StatusBadRequest)),
expectedError: "[400] [somefield] Failed to create domain resource",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- serverURL := setupTest(t, test.mockResponses)
+ provider := test.builder.Build(t)
- assert.NotNil(t, p.client)
- p.client.SetBaseURL(serverURL)
-
- err = p.Present(domain, "", keyAuth)
+ err := provider.Present(domain, "", keyAuth)
if test.expectedError == "" {
assert.NoError(t, err)
} else {
@@ -233,107 +180,111 @@ func TestDNSProvider_CleanUp(t *testing.T) {
defer envTest.RestoreEnv()
os.Setenv(EnvToken, "testing")
- p, err := NewDNSProvider()
- require.NoError(t, err)
-
domain := "example.com"
keyAuth := "dGVzdGluZw=="
testCases := []struct {
desc string
- mockResponses MockResponseMap
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
desc: "Success",
- mockResponses: MockResponseMap{
- "GET:/v4/domains": &linodego.DomainsPagedResponse{
- PageOptions: &linodego.PageOptions{
- Pages: 1,
- Results: 1,
- Page: 1,
- },
- Data: []linodego.Domain{{
- Domain: "foobar.com",
- ID: 1234,
- }},
- },
- "GET:/v4/domains/1234/records": &linodego.DomainRecordsPagedResponse{
- PageOptions: &linodego.PageOptions{
- Pages: 1,
- Results: 1,
- Page: 1,
- },
- Data: []linodego.DomainRecord{{
- ID: 1234,
- Name: "_acme-challenge",
- Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
- Type: "TXT",
- }},
- },
- "DELETE:/v4/domains/1234/records/1234": struct{}{},
- },
+ builder: mockBuilder().
+ Route("GET /v4/domains",
+ servermock.JSONEncode(&linodego.DomainsPagedResponse{
+ PageOptions: &linodego.PageOptions{
+ Pages: 1,
+ Results: 1,
+ Page: 1,
+ },
+ Data: []linodego.Domain{{
+ Domain: "foobar.com",
+ ID: 1234,
+ }},
+ })).
+ Route("GET /v4/domains/1234/records",
+ servermock.JSONEncode(&linodego.DomainRecordsPagedResponse{
+ PageOptions: &linodego.PageOptions{
+ Pages: 1,
+ Results: 1,
+ Page: 1,
+ },
+ Data: []linodego.DomainRecord{{
+ ID: 1234,
+ Name: "_acme-challenge",
+ Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
+ Type: "TXT",
+ }},
+ })).
+ Route("DELETE /v4/domains/1234/records/1234",
+ servermock.RawStringResponse("{}").WithHeader("Content-Type", "application/json")),
},
{
desc: "NoDomain",
- mockResponses: MockResponseMap{
- "GET:/v4/domains": linodego.APIError{
- Errors: []linodego.APIErrorReason{{
- Reason: "Not found",
- }},
- },
- "GET:/v4/domains/1234/records": linodego.APIError{
- Errors: []linodego.APIErrorReason{{
- Reason: "Not found",
- }},
- },
- },
+ builder: mockBuilder().
+ Route("GET /v4/domains",
+ servermock.JSONEncode(linodego.APIError{
+ Errors: []linodego.APIErrorReason{{
+ Reason: "Not found",
+ }},
+ }).
+ WithStatusCode(http.StatusNotFound)).
+ Route("GET /v4/domains/1234/records",
+ servermock.JSONEncode(linodego.APIError{
+ Errors: []linodego.APIErrorReason{{
+ Reason: "Not found",
+ }},
+ },
+ ).
+ WithStatusCode(http.StatusNotFound)),
expectedError: "[404] Not found",
},
{
desc: "DeleteFailed",
- mockResponses: MockResponseMap{
- "GET:/v4/domains": linodego.DomainsPagedResponse{
- PageOptions: &linodego.PageOptions{
- Pages: 1,
- Results: 1,
- Page: 1,
- },
- Data: []linodego.Domain{{
- ID: 1234,
- Domain: "example.com",
- }},
- },
- "GET:/v4/domains/1234/records": linodego.DomainRecordsPagedResponse{
- PageOptions: &linodego.PageOptions{
- Pages: 1,
- Results: 1,
- Page: 1,
- },
- Data: []linodego.DomainRecord{{
- ID: 1234,
- Name: "_acme-challenge",
- Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
- Type: "TXT",
- }},
- },
- "DELETE:/v4/domains/1234/records/1234": linodego.APIError{
- Errors: []linodego.APIErrorReason{{
- Reason: "Failed to delete domain resource",
- }},
- },
- },
+ builder: mockBuilder().
+ Route("GET /v4/domains",
+ servermock.JSONEncode(linodego.DomainsPagedResponse{
+ PageOptions: &linodego.PageOptions{
+ Pages: 1,
+ Results: 1,
+ Page: 1,
+ },
+ Data: []linodego.Domain{{
+ ID: 1234,
+ Domain: "example.com",
+ }},
+ })).
+ Route("GET /v4/domains/1234/records",
+ servermock.JSONEncode(linodego.DomainRecordsPagedResponse{
+ PageOptions: &linodego.PageOptions{
+ Pages: 1,
+ Results: 1,
+ Page: 1,
+ },
+ Data: []linodego.DomainRecord{{
+ ID: 1234,
+ Name: "_acme-challenge",
+ Target: "ElbOJKOkFWiZLQeoxf-wb3IpOsQCdvoM0y_wn0TEkxM",
+ Type: "TXT",
+ }},
+ })).
+ Route("DELETE /v4/domains/1234/records/1234",
+ servermock.JSONEncode(linodego.APIError{
+ Errors: []linodego.APIErrorReason{{
+ Reason: "Failed to delete domain resource",
+ }},
+ }).
+ WithStatusCode(http.StatusBadRequest)),
expectedError: "[400] Failed to delete domain resource",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- serverURL := setupTest(t, test.mockResponses)
+ provider := test.builder.Build(t)
- p.client.SetBaseURL(serverURL)
-
- err = p.CleanUp(domain, "", keyAuth)
+ err := provider.CleanUp(domain, "", keyAuth)
if test.expectedError == "" {
assert.NoError(t, err)
} else {
@@ -356,3 +307,16 @@ func TestLiveCleanUp(t *testing.T) {
}
// TODO implement this test
}
+
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {
+ p, err := NewDNSProvider()
+ if err != nil {
+ return nil, err
+ }
+
+ p.client.SetBaseURL(server.URL)
+
+ return p, nil
+ })
+}
diff --git a/providers/dns/liquidweb/liquidweb_test.go b/providers/dns/liquidweb/liquidweb_test.go
index a26b18e1b..b0788c7f5 100644
--- a/providers/dns/liquidweb/liquidweb_test.go
+++ b/providers/dns/liquidweb/liquidweb_test.go
@@ -18,22 +18,6 @@ var envTest = tester.NewEnvTest(
EnvZone).
WithDomain(envDomain)
-func setupTest(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider {
- t.Helper()
-
- serverURL := mockAPIServer(t, initRecs)
-
- config := NewDefaultConfig()
- config.Username = "blars"
- config.Password = "tacoman"
- config.BaseURL = serverURL
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- return provider
-}
-
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
@@ -161,14 +145,14 @@ func TestNewDNSProviderConfig(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- provider := setupTest(t)
+ provider := mockProvider(t)
err := provider.Present("tacoman.com", "", "")
require.NoError(t, err)
}
func TestDNSProvider_CleanUp(t *testing.T) {
- provider := setupTest(t, network.DNSRecord{
+ provider := mockProvider(t, network.DNSRecord{
Name: "_acme-challenge.tacoman.com",
RData: "123d==",
Type: "TXT",
@@ -239,7 +223,7 @@ func TestDNSProvider(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- provider := setupTest(t, test.initRecs...)
+ provider := mockProvider(t, test.initRecs...)
if test.present {
err := provider.Present(test.domain, test.token, test.keyAuth)
diff --git a/providers/dns/liquidweb/servermock_test.go b/providers/dns/liquidweb/servermock_test.go
index 8c22595af..9cb434761 100644
--- a/providers/dns/liquidweb/servermock_test.go
+++ b/providers/dns/liquidweb/servermock_test.go
@@ -1,7 +1,6 @@
package liquidweb
import (
- "bytes"
"encoding/json"
"fmt"
"io"
@@ -10,11 +9,12 @@ import (
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/liquidweb/liquidweb-go/network"
"github.com/liquidweb/liquidweb-go/types"
)
-func mockAPIServer(t *testing.T, initRecs []network.DNSRecord) string {
+func mockProvider(t *testing.T, initRecs ...network.DNSRecord) *DNSProvider {
t.Helper()
recs := make(map[int]network.DNSRecord)
@@ -23,157 +23,137 @@ func mockAPIServer(t *testing.T, initRecs []network.DNSRecord) string {
recs[int(rec.ID)] = rec
}
- mux := http.NewServeMux()
- mux.Handle("/v1/Network/DNS/Record/delete", mockAPIDelete(recs))
- mux.Handle("/v1/Network/DNS/Record/create", mockAPICreate(recs))
- mux.Handle("/v1/Network/DNS/Zone/list", mockAPIListZones())
- mux.Handle("/bleed/Network/DNS/Record/delete", mockAPIDelete(recs))
- mux.Handle("/bleed/Network/DNS/Record/create", mockAPICreate(recs))
- mux.Handle("/bleed/Network/DNS/Zone/list", mockAPIListZones())
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.Username = "blars"
+ config.Password = "tacoman"
+ config.BaseURL = server.URL
- server := httptest.NewServer(requireBasicAuth(requireJSON(mux)))
- t.Cleanup(server.Close)
-
- return server.URL
-}
-
-func requireBasicAuth(next http.Handler) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- username, password, ok := r.BasicAuth()
- if ok && username == "blars" && password == "tacoman" {
- next.ServeHTTP(w, r)
- return
- }
-
- http.Error(w, "invalid auth", http.StatusForbidden)
- }
-}
-
-func requireJSON(next http.Handler) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- buf := &bytes.Buffer{}
-
- _, err := buf.ReadFrom(r.Body)
- if err != nil {
- http.Error(w, "malformed request - json required", http.StatusBadRequest)
- return
- }
-
- r.Body = io.NopCloser(buf)
- next.ServeHTTP(w, r)
- }
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().
+ WithBasicAuth("blars", "tacoman"),
+ ).
+ Route("/v1/Network/DNS/Record/delete", mockAPIDelete(recs)).
+ Route("/v1/Network/DNS/Record/create", mockAPICreate(recs)).
+ Route("/v1/Network/DNS/Zone/list", mockAPIListZones()).
+ Route("/bleed/Network/DNS/Record/delete", mockAPIDelete(recs)).
+ Route("/bleed/Network/DNS/Record/create", mockAPICreate(recs)).
+ Route("/bleed/Network/DNS/Zone/list", mockAPIListZones()).
+ Build(t)
}
func mockAPICreate(recs map[int]network.DNSRecord) http.HandlerFunc {
_, mockAPIServerZones := makeMockZones()
- return func(w http.ResponseWriter, r *http.Request) {
- body, err := io.ReadAll(r.Body)
+ return func(rw http.ResponseWriter, req *http.Request) {
+ body, err := io.ReadAll(req.Body)
if err != nil {
- http.Error(w, "invalid request", http.StatusInternalServerError)
+ http.Error(rw, "invalid request", http.StatusInternalServerError)
return
}
- req := struct {
+ payload := struct {
Params network.DNSRecord `json:"params"`
}{}
- if err = json.Unmarshal(body, &req); err != nil {
- http.Error(w, makeEncodingError(body), http.StatusBadRequest)
+ if err = json.Unmarshal(body, &payload); err != nil {
+ http.Error(rw, makeEncodingError(body), http.StatusBadRequest)
return
}
- req.Params.ID = types.FlexInt(rand.Intn(10000000))
- req.Params.ZoneID = types.FlexInt(mockAPIServerZones[req.Params.Name])
+ payload.Params.ID = types.FlexInt(rand.Intn(10000000))
+ payload.Params.ZoneID = types.FlexInt(mockAPIServerZones[payload.Params.Name])
- if _, exists := recs[int(req.Params.ID)]; exists {
- http.Error(w, "dns record already exists", http.StatusTeapot)
+ if _, exists := recs[int(payload.Params.ID)]; exists {
+ http.Error(rw, "dns record already exists", http.StatusTeapot)
return
}
- recs[int(req.Params.ID)] = req.Params
+ recs[int(payload.Params.ID)] = payload.Params
- resp, err := json.Marshal(req.Params)
+ resp, err := json.Marshal(payload.Params)
if err != nil {
- http.Error(w, "", http.StatusInternalServerError)
+ http.Error(rw, "", http.StatusInternalServerError)
return
}
- http.Error(w, string(resp), http.StatusOK)
+ http.Error(rw, string(resp), http.StatusOK)
}
}
func mockAPIDelete(recs map[int]network.DNSRecord) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- body, err := io.ReadAll(r.Body)
+ return func(rw http.ResponseWriter, req *http.Request) {
+ body, err := io.ReadAll(req.Body)
if err != nil {
- http.Error(w, "invalid request", http.StatusInternalServerError)
+ http.Error(rw, "invalid request", http.StatusInternalServerError)
return
}
- req := struct {
+ payload := struct {
Params struct {
Name string `json:"name"`
ID int `json:"id"`
} `json:"params"`
}{}
- if err := json.Unmarshal(body, &req); err != nil {
- http.Error(w, makeEncodingError(body), http.StatusBadRequest)
+ if err := json.Unmarshal(body, &payload); err != nil {
+ http.Error(rw, makeEncodingError(body), http.StatusBadRequest)
return
}
- if req.Params.ID == 0 {
- http.Error(w, `{"error":"","error_class":"LW::Exception::Input::Multiple","errors":[{"error":"","error_class":"LW::Exception::Input::Required","field":"id","full_message":"The required field 'id' was missing a value.","position":null}],"field":["id"],"full_message":"The following input errors occurred:\nThe required field 'id' was missing a value.","type":null}`, http.StatusOK)
+ if payload.Params.ID == 0 {
+ http.Error(rw, `{"error":"","error_class":"LW::Exception::Input::Multiple","errors":[{"error":"","error_class":"LW::Exception::Input::Required","field":"id","full_message":"The required field 'id' was missing a value.","position":null}],"field":["id"],"full_message":"The following input errors occurred:\nThe required field 'id' was missing a value.","type":null}`, http.StatusOK)
return
}
- if _, ok := recs[req.Params.ID]; !ok {
- http.Error(w, fmt.Sprintf(`{"error":"","error_class":"LW::Exception::RecordNotFound","field":"network_dns_rr","full_message":"Record 'network_dns_rr: %d' not found","input":"%d","public_message":null}`, req.Params.ID, req.Params.ID), http.StatusOK)
+ if _, ok := recs[payload.Params.ID]; !ok {
+ http.Error(rw, fmt.Sprintf(`{"error":"","error_class":"LW::Exception::RecordNotFound","field":"network_dns_rr","full_message":"Record 'network_dns_rr: %d' not found","input":"%d","public_message":null}`, payload.Params.ID, payload.Params.ID), http.StatusOK)
return
}
- delete(recs, req.Params.ID)
- http.Error(w, fmt.Sprintf("{\"deleted\":%d}", req.Params.ID), http.StatusOK)
+ delete(recs, payload.Params.ID)
+ http.Error(rw, fmt.Sprintf("{\"deleted\":%d}", payload.Params.ID), http.StatusOK)
}
}
func mockAPIListZones() http.HandlerFunc {
mockZones, mockAPIServerZones := makeMockZones()
- return func(w http.ResponseWriter, r *http.Request) {
- body, err := io.ReadAll(r.Body)
+ return func(rw http.ResponseWriter, req *http.Request) {
+ body, err := io.ReadAll(req.Body)
if err != nil {
- http.Error(w, "invalid request", http.StatusInternalServerError)
+ http.Error(rw, "invalid request", http.StatusInternalServerError)
return
}
- req := struct {
+ payload := struct {
Params struct {
PageNum int `json:"page_num"`
} `json:"params"`
}{}
- if err = json.Unmarshal(body, &req); err != nil {
- http.Error(w, makeEncodingError(body), http.StatusBadRequest)
+ if err = json.Unmarshal(body, &payload); err != nil {
+ http.Error(rw, makeEncodingError(body), http.StatusBadRequest)
return
}
switch {
- case req.Params.PageNum < 1:
- req.Params.PageNum = 1
- case req.Params.PageNum > len(mockZones):
- req.Params.PageNum = len(mockZones)
+ case payload.Params.PageNum < 1:
+ payload.Params.PageNum = 1
+ case payload.Params.PageNum > len(mockZones):
+ payload.Params.PageNum = len(mockZones)
}
- resp := mockZones[req.Params.PageNum]
+ resp := mockZones[payload.Params.PageNum]
resp.ItemTotal = types.FlexInt(len(mockAPIServerZones))
- resp.PageNum = types.FlexInt(req.Params.PageNum)
+ resp.PageNum = types.FlexInt(payload.Params.PageNum)
resp.PageSize = 5
resp.PageTotal = types.FlexInt(len(mockZones))
var respBody []byte
if respBody, err = json.Marshal(resp); err == nil {
- http.Error(w, string(respBody), http.StatusOK)
+ http.Error(rw, string(respBody), http.StatusOK)
return
}
- http.Error(w, "", http.StatusInternalServerError)
+ http.Error(rw, "", http.StatusInternalServerError)
}
}
diff --git a/providers/dns/loopia/internal/client_test.go b/providers/dns/loopia/internal/client_test.go
index a84b7c9ad..63962b06e 100644
--- a/providers/dns/loopia/internal/client_test.go
+++ b/providers/dns/loopia/internal/client_test.go
@@ -2,61 +2,76 @@ package internal
import (
"encoding/xml"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+func mockBuilder(password string) *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("apiuser", password)
+ client.BaseURL = server.URL + "/"
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithContentType("text/xml"),
+ )
+}
+
func TestClient_AddZoneRecord(t *testing.T) {
- serverResponses := map[string]string{
- addZoneRecordGoodAuth: responseOk,
- addZoneRecordBadAuth: responseAuthError,
- addZoneRecordNonValidDomain: responseUnknownError,
- addZoneRecordEmptyResponse: "",
- }
-
- serverURL := createFakeServer(t, serverResponses)
-
testCases := []struct {
desc string
password string
domain string
+ request string
+ response string
err string
}{
{
desc: "auth ok",
password: "goodpassword",
domain: exampleDomain,
+ request: addZoneRecordGoodAuth,
+ response: responseOk,
},
{
desc: "auth error",
password: "badpassword",
domain: exampleDomain,
+ request: addZoneRecordBadAuth,
+ response: responseAuthError,
err: "authentication error",
},
{
desc: "unknown error",
password: "goodpassword",
domain: "badexample.com",
+ request: addZoneRecordNonValidDomain,
+ response: responseUnknownError,
err: `unknown error: "UNKNOWN_ERROR"`,
},
{
desc: "empty response",
password: "goodpassword",
domain: "empty.com",
+ request: addZoneRecordEmptyResponse,
+ response: "",
err: "unmarshal error: EOF",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := NewClient("apiuser", test.password)
- client.BaseURL = serverURL + "/"
+ client := mockBuilder(test.password).
+ Route("POST /",
+ servermock.RawStringResponse(test.response),
+ servermock.CheckRequestBody(test.request)).
+ Build(t)
err := client.AddTXTRecord(t.Context(), test.domain, exampleSubDomain, 123, "TXTrecord")
if test.err == "" {
@@ -70,50 +85,54 @@ func TestClient_AddZoneRecord(t *testing.T) {
}
func TestClient_RemoveSubdomain(t *testing.T) {
- serverResponses := map[string]string{
- removeSubdomainGoodAuth: responseOk,
- removeSubdomainBadAuth: responseAuthError,
- removeSubdomainNonValidDomain: responseUnknownError,
- removeSubdomainEmptyResponse: "",
- }
-
- serverURL := createFakeServer(t, serverResponses)
-
testCases := []struct {
desc string
password string
domain string
+ request string
+ response string
err string
}{
{
desc: "auth ok",
password: "goodpassword",
domain: exampleDomain,
+ request: removeSubdomainGoodAuth,
+ response: responseOk,
},
{
desc: "auth error",
password: "badpassword",
domain: exampleDomain,
+ request: removeSubdomainBadAuth,
+ response: responseAuthError,
err: "authentication error",
},
{
desc: "unknown error",
password: "goodpassword",
domain: "badexample.com",
+ request: removeSubdomainNonValidDomain,
+ response: responseUnknownError,
err: `unknown error: "UNKNOWN_ERROR"`,
},
{
desc: "empty response",
password: "goodpassword",
domain: "empty.com",
+ request: removeSubdomainEmptyResponse,
+ response: "",
err: "unmarshal error: EOF",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := NewClient("apiuser", test.password)
- client.BaseURL = serverURL + "/"
+ client := mockBuilder(test.password).
+ Route("POST /",
+ servermock.RawStringResponse(test.response),
+ servermock.CheckRequestBody(test.request)).
+ Build(t)
err := client.RemoveSubdomain(t.Context(), test.domain, exampleSubDomain)
if test.err == "" {
@@ -127,50 +146,54 @@ func TestClient_RemoveSubdomain(t *testing.T) {
}
func TestClient_RemoveZoneRecord(t *testing.T) {
- serverResponses := map[string]string{
- removeRecordGoodAuth: responseOk,
- removeRecordBadAuth: responseAuthError,
- removeRecordNonValidDomain: responseUnknownError,
- removeRecordEmptyResponse: "",
- }
-
- serverURL := createFakeServer(t, serverResponses)
-
testCases := []struct {
desc string
password string
domain string
+ request string
+ response string
err string
}{
{
desc: "auth ok",
password: "goodpassword",
domain: exampleDomain,
+ request: removeRecordGoodAuth,
+ response: responseOk,
},
{
desc: "auth error",
password: "badpassword",
domain: exampleDomain,
+ request: removeRecordBadAuth,
+ response: responseAuthError,
err: "authentication error",
},
{
desc: "uknown error",
password: "goodpassword",
domain: "badexample.com",
+ request: removeRecordNonValidDomain,
+ response: responseUnknownError,
err: `unknown error: "UNKNOWN_ERROR"`,
},
{
desc: "empty response",
password: "goodpassword",
domain: "empty.com",
+ request: removeRecordEmptyResponse,
+ response: "",
err: "unmarshal error: EOF",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := NewClient("apiuser", test.password)
- client.BaseURL = serverURL + "/"
+ client := mockBuilder(test.password).
+ Route("POST /",
+ servermock.RawStringResponse(test.response),
+ servermock.CheckRequestBody(test.request)).
+ Build(t)
err := client.RemoveTXTRecord(t.Context(), test.domain, exampleSubDomain, 12345678)
if test.err == "" {
@@ -184,14 +207,11 @@ func TestClient_RemoveZoneRecord(t *testing.T) {
}
func TestClient_GetZoneRecord(t *testing.T) {
- serverResponses := map[string]string{
- getZoneRecords: getZoneRecordsResponse,
- }
-
- serverURL := createFakeServer(t, serverResponses)
-
- client := NewClient("apiuser", "goodpassword")
- client.BaseURL = serverURL + "/"
+ client := mockBuilder("goodpassword").
+ Route("POST /",
+ servermock.RawStringResponse(getZoneRecordsResponse),
+ servermock.CheckRequestBody(getZoneRecords)).
+ Build(t)
recordObjs, err := client.GetTXTRecords(t.Context(), exampleDomain, exampleSubDomain)
require.NoError(t, err)
@@ -209,23 +229,11 @@ func TestClient_GetZoneRecord(t *testing.T) {
}
func TestClient_rpcCall_404(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.WriteHeader(http.StatusNotFound)
-
- _, err = fmt.Fprint(w, "")
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }))
-
- t.Cleanup(server.Close)
+ client := mockBuilder("apipassword").
+ Route("POST /",
+ servermock.RawStringResponse("").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
call := &methodCall{
MethodName: "dummyMethod",
@@ -234,29 +242,15 @@ func TestClient_rpcCall_404(t *testing.T) {
},
}
- client := NewClient("apiuser", "apipassword")
- client.BaseURL = server.URL + "/"
-
err := client.rpcCall(t.Context(), call, &responseString{})
require.EqualError(t, err, "unexpected status code: [status code: 404] body: ")
}
func TestClient_rpcCall_RPCError(t *testing.T) {
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- _, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- _, err = fmt.Fprint(w, responseRPCError)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }))
-
- t.Cleanup(server.Close)
+ client := mockBuilder("apipassword").
+ Route("POST /",
+ servermock.RawStringResponse(responseRPCError)).
+ Build(t)
call := &methodCall{
MethodName: "getDomains",
@@ -265,9 +259,6 @@ func TestClient_rpcCall_RPCError(t *testing.T) {
},
}
- client := NewClient("apiuser", "apipassword")
- client.BaseURL = server.URL + "/"
-
err := client.rpcCall(t.Context(), call, &responseString{})
require.EqualError(t, err, "RPC Error: (201) Method signature error: 42")
}
@@ -300,37 +291,3 @@ func TestUnmarshallFaultyRecordObject(t *testing.T) {
})
}
}
-
-func createFakeServer(t *testing.T, serverResponses map[string]string) string {
- t.Helper()
-
- handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.Header.Get("Content-Type") != "text/xml" {
- http.Error(w, fmt.Sprintf("invalid content type: %s", r.Header.Get("Content-Type")), http.StatusBadRequest)
- return
- }
-
- req, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- resp, ok := serverResponses[string(req)]
- if !ok {
- http.Error(w, "no response for request", http.StatusBadRequest)
- return
- }
-
- _, err = fmt.Fprint(w, resp)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- return server.URL
-}
diff --git a/providers/dns/luadns/internal/client_test.go b/providers/dns/luadns/internal/client_test.go
index 1b09814ef..0a3a79e6c 100644
--- a/providers/dns/luadns/internal/client_test.go
+++ b/providers/dns/luadns/internal/client_test.go
@@ -1,60 +1,32 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, apiToken string) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder(apiToken string) *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("me", apiToken)
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("me", apiToken)
- client.baseURL, _ = url.Parse(server.URL)
- client.HTTPClient = server.Client()
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("me", apiToken))
}
func TestClient_ListZones(t *testing.T) {
- client, mux := setupTest(t, "secretA")
-
- mux.HandleFunc("/v1/zones", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Basic bWU6c2VjcmV0QQ==" {
- http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open("./fixtures/list_zones.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder("secretA").
+ Route("GET /v1/zones", servermock.ResponseFromFixture("list_zones.json")).
+ Build(t)
zones, err := client.ListZones(t.Context())
require.NoError(t, err)
@@ -88,33 +60,11 @@ func TestClient_ListZones(t *testing.T) {
}
func TestClient_CreateRecord(t *testing.T) {
- client, mux := setupTest(t, "secretB")
-
- mux.HandleFunc("/v1/zones/1/records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Basic bWU6c2VjcmV0Qg==" {
- http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open("./fixtures/create_record.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder("secretB").
+ Route("POST /v1/zones/1/records",
+ servermock.ResponseFromFixture("create_record.json"),
+ servermock.CheckRequestJSONBody(`{"name":"example.com.","type":"MX","content":"10 mail.example.com.","ttl":300}`)).
+ Build(t)
zone := DNSZone{ID: 1}
@@ -141,33 +91,11 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t, "secretC")
-
- mux.HandleFunc("/v1/zones/1/records/2", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Basic bWU6c2VjcmV0Qw==" {
- http.Error(rw, fmt.Sprintf("invalid authentication: %s", auth), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open("./fixtures/delete_record.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder("secretC").
+ Route("DELETE /v1/zones/1/records/2",
+ servermock.ResponseFromFixture("delete_record.json"),
+ servermock.CheckRequestJSONBody(`{"id":2,"name":"example.com.","type":"MX","content":"10 mail.example.com.","ttl":300,"zone_id":1}`)).
+ Build(t)
record := &DNSRecord{
ID: 2,
diff --git a/providers/dns/manageengine/internal/client_test.go b/providers/dns/manageengine/internal/client_test.go
index a47d0b9a8..0c18a245f 100644
--- a/providers/dns/manageengine/internal/client_test.go
+++ b/providers/dns/manageengine/internal/client_test.go
@@ -1,57 +1,35 @@
package internal
import (
- "io"
+ "context"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(context.Background(), "abc", "secret")
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.httpClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client := NewClient(t.Context(), "abc", "secret")
-
- client.httpClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithAccept("application/json"))
}
func TestClient_GetAllZones(t *testing.T) {
- client := setupTest(t, "GET /dns/domain", http.StatusOK, "zone_domains_all.json")
+ client := mockBuilder().
+ Route("GET /dns/domain", servermock.ResponseFromFixture("zone_domains_all.json")).
+ Build(t)
groups, err := client.GetAllZones(t.Context())
require.NoError(t, err)
@@ -132,7 +110,11 @@ func TestClient_GetAllZones(t *testing.T) {
}
func TestClient_GetAllZones_error(t *testing.T) {
- client := setupTest(t, "GET /dns/domain", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("GET /dns/domain",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetAllZones(t.Context())
require.Error(t, err)
@@ -141,7 +123,9 @@ func TestClient_GetAllZones_error(t *testing.T) {
}
func TestClient_GetAllZoneRecords(t *testing.T) {
- client := setupTest(t, "GET /dns/domain/4/records/SPF_TXT", http.StatusOK, "zone_records_all.json")
+ client := mockBuilder().
+ Route("GET /dns/domain/4/records/SPF_TXT", servermock.ResponseFromFixture("zone_records_all.json")).
+ Build(t)
groups, err := client.GetAllZoneRecords(t.Context(), 4)
require.NoError(t, err)
@@ -179,7 +163,11 @@ func TestClient_GetAllZoneRecords(t *testing.T) {
}
func TestClient_GetAllZoneRecords_error(t *testing.T) {
- client := setupTest(t, "GET /dns/domain/4/records/SPF_TXT", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("GET /dns/domain/4/records/SPF_TXT",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetAllZoneRecords(t.Context(), 4)
require.Error(t, err)
@@ -188,14 +176,20 @@ func TestClient_GetAllZoneRecords_error(t *testing.T) {
}
func TestClient_DeleteZoneRecord(t *testing.T) {
- client := setupTest(t, "DELETE /dns/domain/4/records/SPF_TXT/6", http.StatusOK, "zone_record_delete.json")
+ client := mockBuilder().
+ Route("DELETE /dns/domain/4/records/SPF_TXT/6", servermock.ResponseFromFixture("zone_record_delete.json")).
+ Build(t)
err := client.DeleteZoneRecord(t.Context(), 4, 6)
require.NoError(t, err)
}
func TestClient_DeleteZoneRecord_error(t *testing.T) {
- client := setupTest(t, "DELETE /dns/domain/4/records/SPF_TXT/6", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("DELETE /dns/domain/4/records/SPF_TXT/6",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
err := client.DeleteZoneRecord(t.Context(), 4, 6)
require.Error(t, err)
@@ -204,16 +198,45 @@ func TestClient_DeleteZoneRecord_error(t *testing.T) {
}
func TestClient_CreateZoneRecord(t *testing.T) {
- client := setupTest(t, "POST /dns/domain/4/records/SPF_TXT/", http.StatusOK, "zone_record_create.json")
+ client := mockBuilder().
+ Route("POST /dns/domain/4/records/SPF_TXT/",
+ servermock.ResponseFromFixture("zone_record_create.json"),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ servermock.CheckForm().Strict().
+ With("config", `[{"zone_id":1,"spf_txt_domain_id":2,"domain_name":"example.com","domain_ttl":120,"domain_location_id":3,"record_type":"TXT","records":[{"record_id":123,"value":["value1"],"domain_id":1}]}]
+`)).
+ Build(t)
- record := ZoneRecord{}
+ record := ZoneRecord{
+ ZoneID: 1,
+ SpfTxtDomainID: 2,
+ DomainName: "example.com",
+ DomainTTL: 120,
+ DomainLocationID: 3,
+ RecordType: "TXT",
+ Records: []Record{
+ {
+ ID: 123,
+ Values: []string{"value1"},
+ Disabled: false,
+ DomainID: 1,
+ },
+ },
+ }
err := client.CreateZoneRecord(t.Context(), 4, record)
require.NoError(t, err)
}
func TestClient_CreateZoneRecord_error(t *testing.T) {
- client := setupTest(t, "POST /dns/domain/4/records/SPF_TXT/", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("POST /dns/domain/4/records/SPF_TXT/",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded()).
+ Build(t)
record := ZoneRecord{}
@@ -224,7 +247,13 @@ func TestClient_CreateZoneRecord_error(t *testing.T) {
}
func TestClient_CreateZoneRecord_error_bad_request(t *testing.T) {
- client := setupTest(t, "POST /dns/domain/4/records/SPF_TXT/", http.StatusBadRequest, "error_bad_request.json")
+ client := mockBuilder().
+ Route("POST /dns/domain/4/records/SPF_TXT/",
+ servermock.ResponseFromFixture("error_bad_request.json").
+ WithStatusCode(http.StatusBadRequest),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded()).
+ Build(t)
record := ZoneRecord{}
@@ -235,7 +264,15 @@ func TestClient_CreateZoneRecord_error_bad_request(t *testing.T) {
}
func TestClient_UpdateZoneRecord(t *testing.T) {
- client := setupTest(t, "PUT /dns/domain/4/records/SPF_TXT/6/", http.StatusOK, "zone_record_update.json")
+ client := mockBuilder().
+ Route("PUT /dns/domain/4/records/SPF_TXT/6/",
+ servermock.ResponseFromFixture("zone_record_update.json"),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ servermock.CheckForm().Strict().
+ With("config", `[{"zone_id":4,"spf_txt_domain_id":6,"records":null}]
+`)).
+ Build(t)
record := ZoneRecord{
SpfTxtDomainID: 6,
@@ -247,7 +284,13 @@ func TestClient_UpdateZoneRecord(t *testing.T) {
}
func TestClient_UpdateZoneRecord_error(t *testing.T) {
- client := setupTest(t, "PUT /dns/domain/4/records/SPF_TXT/6/", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("PUT /dns/domain/4/records/SPF_TXT/6/",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded()).
+ Build(t)
record := ZoneRecord{
SpfTxtDomainID: 6,
diff --git a/providers/dns/metaregistrar/internal/client.go b/providers/dns/metaregistrar/internal/client.go
index f2838c532..4d3c8adc4 100644
--- a/providers/dns/metaregistrar/internal/client.go
+++ b/providers/dns/metaregistrar/internal/client.go
@@ -16,6 +16,8 @@ import (
const defaultBaseURL = "https://api.metaregistrar.com"
+const tokenHeader = "token"
+
// Client is a client to interact with the Metaregistrar API.
type Client struct {
token string
@@ -61,7 +63,7 @@ func (c Client) UpdateDNSZone(ctx context.Context, domain string, updateRequest
}
func (c Client) do(req *http.Request, result any) error {
- req.Header.Add("token", c.token)
+ req.Header.Add(tokenHeader, c.token)
resp, err := c.HTTPClient.Do(req)
if err != nil {
diff --git a/providers/dns/metaregistrar/internal/client_test.go b/providers/dns/metaregistrar/internal/client_test.go
index 8486fc899..33e92cd7b 100644
--- a/providers/dns/metaregistrar/internal/client_test.go
+++ b/providers/dns/metaregistrar/internal/client_test.go
@@ -1,58 +1,39 @@
package internal
import (
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("token")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With(tokenHeader, "secret"))
}
func TestClient_UpdateDNSZone(t *testing.T) {
- client := setupTest(t, "PATCH /dnszone/example.com", http.StatusOK, "update-dns-zone.json")
+ client := mockBuilder().
+ Route("PATCH /dnszone/example.com",
+ servermock.ResponseFromFixture("update-dns-zone.json"),
+ servermock.CheckRequestJSONBody(`{"add":[{"name":"@","type":"TXT","ttl":60,"content":"value"}]}`)).
+ Build(t)
updateRequest := DNSZoneUpdateRequest{
Add: []Record{{
@@ -95,7 +76,11 @@ func TestClient_UpdateDNSZone_error(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := setupTest(t, "PATCH /dnszone/example.com", http.StatusUnprocessableEntity, test.filename)
+ client := mockBuilder().
+ Route("PATCH /dnszone/example.com",
+ servermock.ResponseFromFixture(test.filename).
+ WithStatusCode(http.StatusUnprocessableEntity)).
+ Build(t)
updateRequest := DNSZoneUpdateRequest{
Add: []Record{{
diff --git a/providers/dns/mijnhost/internal/client_test.go b/providers/dns/mijnhost/internal/client_test.go
index a1dc326b7..208616541 100644
--- a/providers/dns/mijnhost/internal/client_test.go
+++ b/providers/dns/mijnhost/internal/client_test.go
@@ -1,69 +1,35 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const apiKey = "secret"
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(apiKey)
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(apiKey)
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func testHandler(filename, method string, statusCode int) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != apiKey {
- http.Error(rw, "invalid Authorization header", http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With(authorizationHeader, apiKey),
+ )
}
func TestClient_ListDomains(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains", testHandler("./list-domains.json", http.MethodGet, http.StatusOK))
+ client := mockBuilder().
+ Route("GET /domains", servermock.ResponseFromFixture("list-domains.json")).
+ Build(t)
domains, err := client.ListDomains(t.Context())
require.NoError(t, err)
@@ -81,9 +47,9 @@ func TestClient_ListDomains(t *testing.T) {
}
func TestClient_GetRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/domains/example.com/dns", testHandler("./get-dns-records.json", http.MethodGet, http.StatusOK))
+ client := mockBuilder().
+ Route("GET /domains/example.com/dns", servermock.ResponseFromFixture("get-dns-records.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "example.com")
require.NoError(t, err)
@@ -119,10 +85,19 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_UpdateRecords(t *testing.T) {
- client, mux := setupTest(t)
+ client := mockBuilder().
+ Route("PUT /domains/example.com/dns",
+ servermock.ResponseFromFixture("update-dns-records.json"),
+ servermock.CheckRequestJSONBody(`{"records":[{"type":"TXT","name":"foo","value":"value1","ttl":120}]}`)).
+ Build(t)
- mux.HandleFunc("/domains/example.com/dns", testHandler("./update-dns-records.json", http.MethodPut, http.StatusOK))
+ records := []Record{{
+ Type: "TXT",
+ Name: "foo",
+ Value: "value1",
+ TTL: 120,
+ }}
- err := client.UpdateRecords(t.Context(), "example.com", nil)
+ err := client.UpdateRecords(t.Context(), "example.com", records)
require.NoError(t, err)
}
diff --git a/providers/dns/mittwald/internal/client_test.go b/providers/dns/mittwald/internal/client_test.go
index f73a36cc1..e57c80f7a 100644
--- a/providers/dns/mittwald/internal/client_test.go
+++ b/providers/dns/mittwald/internal/client_test.go
@@ -1,72 +1,34 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, handler)
-
- client := NewClient("secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
-}
-
-func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "Bearer secret" {
- http.Error(rw, fmt.Sprintf("invalid API Token: %s", auth), http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(statusCode)
-
- if statusCode == http.StatusNoContent {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
- }
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer secret"),
+ )
}
func TestClient_ListDomains(t *testing.T) {
- client := setupTest(t, "/domains", testHandler(http.MethodGet, http.StatusOK, "domain-list-domains.json"))
+ client := mockBuilder().
+ Route("GET /domains", servermock.ResponseFromFixture("domain-list-domains.json")).
+ Build(t)
domains, err := client.ListDomains(t.Context())
require.NoError(t, err)
@@ -83,14 +45,20 @@ func TestClient_ListDomains(t *testing.T) {
}
func TestClient_ListDomains_error(t *testing.T) {
- client := setupTest(t, "/domains", testHandler(http.MethodGet, http.StatusBadRequest, "error-client.json"))
+ client := mockBuilder().
+ Route("GET /domains",
+ servermock.ResponseFromFixture("error-client.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
_, err := client.ListDomains(t.Context())
require.EqualError(t, err, "[status code 400] ValidationError: Validation failed [format: should be string (.address.street, email)]")
}
func TestClient_ListDNSZones(t *testing.T) {
- client := setupTest(t, "/projects/my-project-id/dns-zones", testHandler(http.MethodGet, http.StatusOK, "dns-list-dns-zones.json"))
+ client := mockBuilder().
+ Route("GET /projects/my-project-id/dns-zones", servermock.ResponseFromFixture("dns-list-dns-zones.json")).
+ Build(t)
zones, err := client.ListDNSZones(t.Context(), "my-project-id")
require.NoError(t, err)
@@ -109,7 +77,9 @@ func TestClient_ListDNSZones(t *testing.T) {
}
func TestClient_GetDNSZone(t *testing.T) {
- client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodGet, http.StatusOK, "dns-get-dns-zone.json"))
+ client := mockBuilder().
+ Route("GET /dns-zones/my-zone-id", servermock.ResponseFromFixture("dns-get-dns-zone.json")).
+ Build(t)
zone, err := client.GetDNSZone(t.Context(), "my-zone-id")
require.NoError(t, err)
@@ -126,7 +96,11 @@ func TestClient_GetDNSZone(t *testing.T) {
}
func TestClient_CreateDNSZone(t *testing.T) {
- client := setupTest(t, "/dns-zones", testHandler(http.MethodPost, http.StatusCreated, "dns-create-dns-zone.json"))
+ client := mockBuilder().
+ Route("POST /dns-zones",
+ servermock.ResponseFromFixture("dns-create-dns-zone.json"),
+ servermock.CheckRequestJSONBody(`{"name":"test","parentZoneId":"my-parent-zone-id"}`)).
+ Build(t)
request := CreateDNSZoneRequest{
Name: "test",
@@ -144,7 +118,12 @@ func TestClient_CreateDNSZone(t *testing.T) {
}
func TestClient_UpdateTXTRecord(t *testing.T) {
- client := setupTest(t, "/dns-zones/my-zone-id/record-sets/txt", testHandler(http.MethodPut, http.StatusNoContent, ""))
+ client := mockBuilder().
+ Route("PUT /dns-zones/my-zone-id/record-sets/txt",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent),
+ servermock.CheckRequestJSONBody(`{"settings":{"ttl":{"auto":true}},"entries":["txt"]}`)).
+ Build(t)
record := TXTRecord{
Settings: Settings{
@@ -158,14 +137,21 @@ func TestClient_UpdateTXTRecord(t *testing.T) {
}
func TestClient_DeleteDNSZone(t *testing.T) {
- client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodDelete, http.StatusOK, ""))
+ client := mockBuilder().
+ Route("DELETE /dns-zones/my-zone-id",
+ servermock.Noop()).
+ Build(t)
err := client.DeleteDNSZone(t.Context(), "my-zone-id")
require.NoError(t, err)
}
func TestClient_DeleteDNSZone_error(t *testing.T) {
- client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodDelete, http.StatusInternalServerError, "error.json"))
+ client := mockBuilder().
+ Route("DELETE /dns-zones/my-zone-id",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusInternalServerError)).
+ Build(t)
err := client.DeleteDNSZone(t.Context(), "my-zone-id")
assert.EqualError(t, err, "[status code 500] InternalServerError: Something went wrong")
diff --git a/providers/dns/myaddr/internal/client_test.go b/providers/dns/myaddr/internal/client_test.go
index 794a501fb..36506d94a 100644
--- a/providers/dns/myaddr/internal/client_test.go
+++ b/providers/dns/myaddr/internal/client_test.go
@@ -1,75 +1,61 @@
package internal
import (
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ credentials := map[string]string{
+ "example": "secret",
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client, err := NewClient(credentials)
+ if err != nil {
+ return nil, err
+ }
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- credentials := map[string]string{
- "example": "secret",
- }
-
- client, err := NewClient(credentials)
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestClient_AddTXTRecord(t *testing.T) {
- client := setupTest(t, "POST /update", http.StatusOK, "")
+ client := mockBuilder().
+ Route("POST /update", nil,
+ servermock.CheckRequestJSONBody(`{"key":"secret","acme_challenge":"txt"}`)).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "example", "txt")
require.NoError(t, err)
}
func TestClient_AddTXTRecord_error(t *testing.T) {
- client := setupTest(t, "POST /update", http.StatusBadRequest, "error.txt")
+ client := mockBuilder().
+ Route("POST /update",
+ servermock.ResponseFromFixture("error.txt").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "example", "txt")
require.EqualError(t, err, `unexpected status code: [status code: 400] body: invalid value for "key"`)
}
func TestClient_AddTXTRecord_error_credentials(t *testing.T) {
- client := setupTest(t, "POST /update", http.StatusOK, "")
+ client := mockBuilder().
+ Route("POST /update", nil).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "nx", "txt")
require.EqualError(t, err, "subdomain nx not found in credentials, check your credentials map")
diff --git a/providers/dns/mydnsjp/internal/client_test.go b/providers/dns/mydnsjp/internal/client_test.go
index a0f9ab8c7..41ccbba87 100644
--- a/providers/dns/mydnsjp/internal/client_test.go
+++ b/providers/dns/mydnsjp/internal/client_test.go
@@ -1,90 +1,49 @@
package internal
import (
- "fmt"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, cmdName string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("xxx", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("invalid method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- username, password, ok := req.BasicAuth()
- if !ok {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if username != "xxx" {
- http.Error(rw, fmt.Sprintf("username: want %s got %s", username, "xxx"), http.StatusUnauthorized)
- return
- }
-
- if password != "secret" {
- http.Error(rw, fmt.Sprintf("password: want %s got %s", password, "secret"), http.StatusUnauthorized)
- return
- }
-
- if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
- http.Error(rw, fmt.Sprintf("invalid Content-Type: %s", req.Header.Get("Content-Type")), http.StatusBadRequest)
- return
- }
-
- err := req.ParseForm()
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- domain := req.Form.Get("CERTBOT_DOMAIN")
- if domain != "example.com" {
- http.Error(rw, fmt.Sprintf("unexpected CERTBOT_DOMAIN: %s", domain), http.StatusBadRequest)
- return
- }
-
- validation := req.Form.Get("CERTBOT_VALIDATION")
- if validation != "txt" {
- http.Error(rw, fmt.Sprintf("unexpected CERTBOT_VALIDATION: %s", validation), http.StatusBadRequest)
- return
- }
-
- cmd := req.Form.Get("EDIT_CMD")
- if cmd != cmdName {
- http.Error(rw, fmt.Sprintf("unexpected EDIT_CMD: %s", cmd), http.StatusBadRequest)
- return
- }
- })
-
- client := NewClient("xxx", "secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded().
+ WithBasicAuth("xxx", "secret"))
}
func TestClient_AddTXTRecord(t *testing.T) {
- client := setupTest(t, "REGIST")
+ client := mockBuilder().
+ Route("POST /", nil,
+ servermock.CheckForm().Strict().
+ With("CERTBOT_DOMAIN", "example.com").
+ With("CERTBOT_VALIDATION", "txt").
+ With("EDIT_CMD", "REGIST")).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "example.com", "txt")
require.NoError(t, err)
}
func TestClient_DeleteTXTRecord(t *testing.T) {
- client := setupTest(t, "DELETE")
+ client := mockBuilder().
+ Route("POST /", nil,
+ servermock.CheckForm().Strict().
+ With("CERTBOT_DOMAIN", "example.com").
+ With("CERTBOT_VALIDATION", "txt").
+ With("EDIT_CMD", "DELETE")).
+ Build(t)
err := client.DeleteTXTRecord(t.Context(), "example.com", "txt")
require.NoError(t, err)
diff --git a/providers/dns/mythicbeasts/internal/client_test.go b/providers/dns/mythicbeasts/internal/client_test.go
index 1e5f83e3c..acbf85268 100644
--- a/providers/dns/mythicbeasts/internal/client_test.go
+++ b/providers/dns/mythicbeasts/internal/client_test.go
@@ -1,68 +1,53 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
"time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.APIEndpoint, _ = url.Parse(server.URL)
+ client.token = &Token{
+ Token: "secret",
+ Lifetime: 60,
+ TokenType: "bearer",
+ Deadline: time.Now().Add(1 * time.Minute),
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, handler)
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.APIEndpoint, _ = url.Parse(server.URL)
- client.token = &Token{
- Token: "secret",
- Lifetime: 60,
- TokenType: "bearer",
- Deadline: time.Now().Add(1 * time.Minute),
- }
-
- return client
-}
-
-func writeFixtureHandler(method, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
- }
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer "+fakeToken),
+ )
}
func TestClient_CreateTXTRecord(t *testing.T) {
- client := setupTest(t, "/zones/example.com/records/foo/TXT", writeFixtureHandler(http.MethodPost, "post-zoneszonerecords.json"))
+ client := mockBuilder().
+ Route("POST /zones/example.com/records/foo/TXT",
+ servermock.ResponseFromFixture("post-zoneszonerecords.json"),
+ servermock.CheckRequestJSONBody(`{"records":[{"host":"foo","ttl":120,"type":"TXT","data":"txt"}]}`)).
+ Build(t)
err := client.CreateTXTRecord(mockContext(t), "example.com", "foo", "txt", 120)
require.NoError(t, err)
}
func TestClient_RemoveTXTRecord(t *testing.T) {
- client := setupTest(t, "/zones/example.com/records/foo/TXT", writeFixtureHandler(http.MethodDelete, "delete-zoneszonerecords.json"))
+ client := mockBuilder().
+ Route("DELETE /zones/example.com/records/foo/TXT",
+ servermock.ResponseFromFixture("delete-zoneszonerecords.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("data", "txt")).
+ Build(t)
err := client.RemoveTXTRecord(mockContext(t), "example.com", "foo", "txt")
require.NoError(t, err)
diff --git a/providers/dns/mythicbeasts/internal/fixtures/token.json b/providers/dns/mythicbeasts/internal/fixtures/token.json
new file mode 100644
index 000000000..f23fe58ea
--- /dev/null
+++ b/providers/dns/mythicbeasts/internal/fixtures/token.json
@@ -0,0 +1,5 @@
+{
+ "access_token": "xxx",
+ "expires_in": 666,
+ "token_type": "bearer"
+}
diff --git a/providers/dns/mythicbeasts/internal/identity_test.go b/providers/dns/mythicbeasts/internal/identity_test.go
index e26bad6aa..3e1e8ba4f 100644
--- a/providers/dns/mythicbeasts/internal/identity_test.go
+++ b/providers/dns/mythicbeasts/internal/identity_test.go
@@ -2,52 +2,45 @@ package internal
import (
"context"
- "encoding/json"
- "fmt"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+const fakeToken = "xxx"
+
func mockContext(t *testing.T) context.Context {
t.Helper()
- return context.WithValue(t.Context(), tokenKey, &Token{Token: "xxx"})
+ return context.WithValue(t.Context(), tokenKey, &Token{Token: fakeToken})
}
-func tokenHandler(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, http.MethodPost), http.StatusMethodNotAllowed)
- return
- }
+func mockBuilderIdentity() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.AuthEndpoint, _ = url.Parse(server.URL)
- username, password, ok := req.BasicAuth()
- if !ok || username != "user" || password != "secret" {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- _ = json.NewEncoder(rw).Encode(Token{
- Token: "xxx",
- Lifetime: 666,
- TokenType: "bearer",
- })
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithBasicAuth("user", "secret"),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded())
}
func TestClient_obtainToken(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", tokenHandler)
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.AuthEndpoint, _ = url.Parse(server.URL)
+ client := mockBuilderIdentity().
+ Route("POST /",
+ servermock.ResponseFromFixture("token.json"),
+ servermock.CheckForm().Strict().
+ With("grant_type", "client_credentials")).
+ Build(t)
assert.Nil(t, client.token)
@@ -56,19 +49,16 @@ func TestClient_obtainToken(t *testing.T) {
assert.NotNil(t, tok)
assert.NotZero(t, tok.Deadline)
- assert.Equal(t, "xxx", tok.Token)
+ assert.Equal(t, fakeToken, tok.Token)
}
func TestClient_CreateAuthenticatedContext(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", tokenHandler)
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.AuthEndpoint, _ = url.Parse(server.URL)
+ client := mockBuilderIdentity().
+ Route("POST /",
+ servermock.ResponseFromFixture("token.json"),
+ servermock.CheckForm().Strict().
+ With("grant_type", "client_credentials")).
+ Build(t)
assert.Nil(t, client.token)
@@ -79,5 +69,5 @@ func TestClient_CreateAuthenticatedContext(t *testing.T) {
assert.NotNil(t, tok)
assert.NotZero(t, tok.Deadline)
- assert.Equal(t, "xxx", tok.Token)
+ assert.Equal(t, fakeToken, tok.Token)
}
diff --git a/providers/dns/namecheap/internal/client_test.go b/providers/dns/namecheap/internal/client_test.go
index 6a6ba201a..d7bea7b6e 100644
--- a/providers/dns/namecheap/internal/client_test.go
+++ b/providers/dns/namecheap/internal/client_test.go
@@ -1,72 +1,36 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, handler http.HandlerFunc) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", handler)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("user", "secret", "127.0.0.1")
client.HTTPClient = server.Client()
client.BaseURL = server.URL
- return client
-}
-
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
+ return client, nil
}
func TestClient_GetHosts(t *testing.T) {
- client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- expectedParams := map[string]string{
- "ApiKey": "secret",
- "ApiUser": "user",
- "ClientIp": "127.0.0.1",
- "Command": "namecheap.domains.dns.getHosts",
- "SLD": "foo",
- "TLD": "example.com",
- "UserName": "user",
- }
-
- query := req.URL.Query()
- for k, v := range expectedParams {
- if query.Get(k) != v {
- http.Error(rw, fmt.Sprintf("invalid query parameter %s value: %s", k, query.Get(k)), http.StatusBadRequest)
- return
- }
- }
-
- writeFixture(rw, "getHosts.xml")
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /",
+ servermock.ResponseFromFixture("getHosts.xml"),
+ servermock.CheckQueryParameter().Strict().
+ With("ApiKey", "secret").
+ With("ApiUser", "user").
+ With("ClientIp", "127.0.0.1").
+ With("Command", "namecheap.domains.dns.getHosts").
+ With("SLD", "foo").
+ With("TLD", "example.com").
+ With("UserName", "user"),
+ ).
+ Build(t)
hosts, err := client.GetHosts(t.Context(), "foo", "example.com")
require.NoError(t, err)
@@ -80,68 +44,41 @@ func TestClient_GetHosts(t *testing.T) {
}
func TestClient_GetHosts_error(t *testing.T) {
- client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, "getHosts_errorBadAPIKey1.xml")
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /",
+ servermock.ResponseFromFixture("getHosts_errorBadAPIKey1.xml")).
+ Build(t)
_, err := client.GetHosts(t.Context(), "foo", "example.com")
require.ErrorAs(t, err, &apiError{})
}
func TestClient_SetHosts(t *testing.T) {
- client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
- http.Error(rw, fmt.Sprintf("invalid Content-Type: %s", req.Header.Get("Content-Type")), http.StatusBadRequest)
- return
- }
-
- err := req.ParseForm()
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- expectedParams := map[string]string{
- "HostName1": "_acme-challenge.test.example.com",
- "RecordType1": "TXT",
- "Address1": "txtTXTtxt",
- "MXPref1": "10",
- "TTL1": "120",
-
- "HostName2": "_acme-challenge.test.example.org",
- "RecordType2": "TXT",
- "Address2": "txtTXTtxt",
- "MXPref2": "10",
- "TTL2": "120",
-
- "ApiKey": "secret",
- "ApiUser": "user",
- "ClientIp": "127.0.0.1",
- "Command": "namecheap.domains.dns.setHosts",
- "SLD": "foo",
- "TLD": "example.com",
- "UserName": "user",
- }
-
- for k, v := range expectedParams {
- if req.Form.Get(k) != v {
- http.Error(rw, fmt.Sprintf("invalid form data %s value: %q", k, req.Form.Get(k)), http.StatusBadRequest)
- return
- }
- }
-
- writeFixture(rw, "setHosts.xml")
- })
+ client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithContentTypeFromURLEncoded()).
+ Route("POST /",
+ servermock.ResponseFromFixture("setHosts.xml"),
+ servermock.CheckForm().Strict().
+ With("ApiKey", "secret").
+ With("ApiUser", "user").
+ With("ClientIp", "127.0.0.1").
+ With("Command", "namecheap.domains.dns.setHosts").
+ With("SLD", "foo").
+ With("TLD", "example.com").
+ With("UserName", "user").
+ // entry 1
+ With("HostName1", "_acme-challenge.test.example.com").
+ With("RecordType1", "TXT").
+ With("Address1", "txtTXTtxt").
+ With("MXPref1", "10").
+ With("TTL1", "120").
+ // entry 2
+ With("HostName2", "_acme-challenge.test.example.org").
+ With("RecordType2", "TXT").
+ With("Address2", "txtTXTtxt").
+ With("MXPref2", "10").
+ With("TTL2", "120"),
+ ).
+ Build(t)
records := []Record{
{Name: "_acme-challenge.test.example.com", Type: "TXT", Address: "txtTXTtxt", MXPref: "10", TTL: "120"},
@@ -153,14 +90,10 @@ func TestClient_SetHosts(t *testing.T) {
}
func TestClient_SetHosts_error(t *testing.T) {
- client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- writeFixture(rw, "setHosts_errorBadAPIKey1.xml")
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /",
+ servermock.ResponseFromFixture("setHosts_errorBadAPIKey1.xml")).
+ Build(t)
records := []Record{
{Name: "_acme-challenge.test.example.com", Type: "TXT", Address: "txtTXTtxt", MXPref: "10", TTL: "120"},
diff --git a/providers/dns/namecheap/namecheap_test.go b/providers/dns/namecheap/namecheap_test.go
index 01f87aaf0..fedbc3162 100644
--- a/providers/dns/namecheap/namecheap_test.go
+++ b/providers/dns/namecheap/namecheap_test.go
@@ -1,16 +1,13 @@
package namecheap
import (
- "io"
"net/http"
"net/http/httptest"
- "net/url"
- "os"
"path/filepath"
"testing"
"time"
- "github.com/go-acme/lego/v4/providers/dns/namecheap/internal"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -24,7 +21,6 @@ const (
type testCase struct {
name string
domain string
- hosts []internal.Record
errString string
getHostsResponse string
setHostsResponse string
@@ -32,26 +28,14 @@ type testCase struct {
var testCases = []testCase{
{
- name: "Test:Success:1",
- domain: "test.example.com",
- hosts: []internal.Record{
- {Type: "A", Name: "home", Address: "10.0.0.1", MXPref: "10", TTL: "1799"},
- {Type: "A", Name: "www", Address: "10.0.0.2", MXPref: "10", TTL: "1200"},
- {Type: "AAAA", Name: "a", Address: "::0", MXPref: "10", TTL: "1799"},
- {Type: "CNAME", Name: "*", Address: "example.com.", MXPref: "10", TTL: "1799"},
- {Type: "MXE", Name: "example.com", Address: "10.0.0.5", MXPref: "10", TTL: "1800"},
- {Type: "URL", Name: "xyz", Address: "https://google.com", MXPref: "10", TTL: "1799"},
- },
+ name: "Test:Success:1",
+ domain: "test.example.com",
getHostsResponse: "getHosts_success1.xml",
setHostsResponse: "setHosts_success1.xml",
},
{
- name: "Test:Success:2",
- domain: "example.com",
- hosts: []internal.Record{
- {Type: "A", Name: "@", Address: "10.0.0.2", MXPref: "10", TTL: "1200"},
- {Type: "A", Name: "www", Address: "10.0.0.3", MXPref: "10", TTL: "60"},
- },
+ name: "Test:Success:2",
+ domain: "example.com",
getHostsResponse: "getHosts_success2.xml",
setHostsResponse: "setHosts_success2.xml",
},
@@ -63,96 +47,37 @@ var testCases = []testCase{
},
}
-func setupTest(t *testing.T, tc *testCase) *DNSProvider {
- t.Helper()
-
- handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case http.MethodGet:
- values := r.URL.Query()
- cmd := values.Get("Command")
- switch cmd {
- case "namecheap.domains.dns.getHosts":
- assertHdr(t, tc, &values)
- w.WriteHeader(http.StatusOK)
- writeFixture(w, tc.getHostsResponse)
- default:
- t.Errorf("Unexpected GET command: %s", cmd)
- }
-
- case http.MethodPost:
- err := r.ParseForm()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- values := r.Form
- cmd := values.Get("Command")
- switch cmd {
- case "namecheap.domains.dns.setHosts":
- assertHdr(t, tc, &values)
- w.WriteHeader(http.StatusOK)
- writeFixture(w, tc.setHostsResponse)
- default:
- t.Errorf("Unexpected POST command: %s", cmd)
- }
-
- default:
- t.Errorf("Unexpected http method: %s", r.Method)
- }
- })
-
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- return mockDNSProvider(t, server.URL)
-}
-
-func mockDNSProvider(t *testing.T, baseURL string) *DNSProvider {
- t.Helper()
-
- config := NewDefaultConfig()
- config.BaseURL = baseURL
- config.APIUser = envTestUser
- config.APIKey = envTestKey
- config.ClientIP = envTestClientIP
- config.HTTPClient = &http.Client{Timeout: 60 * time.Second}
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- return provider
-}
-
-func assertHdr(t *testing.T, tc *testCase, values *url.Values) {
- t.Helper()
-
- ch, _ := newPseudoRecord(tc.domain, "")
- assert.Equal(t, envTestUser, values.Get("ApiUser"), "ApiUser")
- assert.Equal(t, envTestKey, values.Get("ApiKey"), "ApiKey")
- assert.Equal(t, envTestUser, values.Get("UserName"), "UserName")
- assert.Equal(t, envTestClientIP, values.Get("ClientIp"), "ClientIp")
- assert.Equal(t, ch.sld, values.Get("SLD"), "SLD")
- assert.Equal(t, ch.tld, values.Get("TLD"), "TLD")
-}
-
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("internal", "fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
-}
-
func TestDNSProvider_Present(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
- p := setupTest(t, &test)
+ ch, _ := newPseudoRecord(test.domain, "")
- err := p.Present(test.domain, "", "dummyKey")
+ provider := mockBuilder().
+ Route("GET /",
+ servermock.ResponseFromFile(filepath.Join("internal", "fixtures", test.getHostsResponse)),
+ servermock.CheckForm().Strict().
+ With("ClientIp", "10.0.0.1").
+ With("Command", "namecheap.domains.dns.getHosts").
+ With("SLD", ch.sld).
+ With("TLD", ch.tld).
+ With("UserName", "foo").
+ With("ApiKey", "bar").
+ With("ApiUser", "foo"),
+ ).
+ Route("POST /",
+ servermock.ResponseFromFile(filepath.Join("internal", "fixtures", test.setHostsResponse)),
+ servermock.CheckForm().
+ With("ClientIp", "10.0.0.1").
+ With("Command", "namecheap.domains.dns.setHosts").
+ With("SLD", ch.sld).
+ With("TLD", ch.tld).
+ With("UserName", "foo").
+ With("ApiKey", "bar").
+ With("ApiUser", "foo"),
+ ).
+ Build(t)
+
+ err := provider.Present(test.domain, "", "dummyKey")
if test.errString != "" {
assert.EqualError(t, err, "namecheap: "+test.errString)
} else {
@@ -165,9 +90,34 @@ func TestDNSProvider_Present(t *testing.T) {
func TestDNSProvider_CleanUp(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
- p := setupTest(t, &test)
+ ch, _ := newPseudoRecord(test.domain, "")
- err := p.CleanUp(test.domain, "", "dummyKey")
+ provider := mockBuilder().
+ Route("GET /",
+ servermock.ResponseFromFile(filepath.Join("internal", "fixtures", test.getHostsResponse)),
+ servermock.CheckForm().Strict().
+ With("ClientIp", "10.0.0.1").
+ With("Command", "namecheap.domains.dns.getHosts").
+ With("SLD", ch.sld).
+ With("TLD", ch.tld).
+ With("UserName", "foo").
+ With("ApiKey", "bar").
+ With("ApiUser", "foo"),
+ ).
+ Route("POST /",
+ servermock.ResponseFromFile(filepath.Join("internal", "fixtures", test.setHostsResponse)),
+ servermock.CheckForm().
+ With("ClientIp", "10.0.0.1").
+ With("Command", "namecheap.domains.dns.setHosts").
+ With("SLD", ch.sld).
+ With("TLD", ch.tld).
+ With("UserName", "foo").
+ With("ApiKey", "bar").
+ With("ApiUser", "foo"),
+ ).
+ Build(t)
+
+ err := provider.CleanUp(test.domain, "", "dummyKey")
if test.errString != "" {
assert.EqualError(t, err, "namecheap: "+test.errString)
} else {
@@ -226,3 +176,16 @@ func Test_newPseudoRecord_domainSplit(t *testing.T) {
})
}
}
+
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.BaseURL = server.URL
+ config.APIUser = envTestUser
+ config.APIKey = envTestKey
+ config.ClientIP = envTestClientIP
+ config.HTTPClient = &http.Client{Timeout: 60 * time.Second}
+
+ return NewDNSProviderConfig(config)
+ })
+}
diff --git a/providers/dns/nearlyfreespeech/internal/client_test.go b/providers/dns/nearlyfreespeech/internal/client_test.go
index 9c0329978..1445286c3 100644
--- a/providers/dns/nearlyfreespeech/internal/client_test.go
+++ b/providers/dns/nearlyfreespeech/internal/client_test.go
@@ -1,26 +1,18 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
"time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("user", "secret")
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
@@ -28,66 +20,22 @@ func setupTest(t *testing.T) (*Client, *http.ServeMux) {
client.signer.saltShaker = func() []byte { return []byte("0123456789ABCDEF") }
client.signer.clock = func() time.Time { return time.Unix(1692475113, 0) }
- return client, mux
-}
-
-func testHandler(params map[string]string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- if req.Header.Get(authenticationHeader) == "" {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- err := req.ParseForm()
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- for k, v := range params {
- if req.PostForm.Get(k) != v {
- http.Error(rw, fmt.Sprintf("data: got %s want %s", k, v), http.StatusBadRequest)
- return
- }
- }
- }
-}
-
-func testErrorHandler() http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- file, err := os.Open("./fixtures/error.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- rw.WriteHeader(http.StatusUnauthorized)
-
- _, _ = io.Copy(rw, file)
- }
+ return client, nil
}
func TestClient_AddRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- params := map[string]string{
- "data": "txtTXTtxt",
- "name": "sub",
- "type": "TXT",
- "ttl": "30",
- }
-
- mux.Handle("/dns/example.com/addRR", testHandler(params))
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded().
+ With(authenticationHeader, "user;1692475113;0123456789ABCDEF;24a32faf74c7bd0525f560ff12a1c1fb6545bafc"),
+ ).
+ Route("POST /dns/example.com/addRR", nil, servermock.CheckForm().Strict().
+ With("data", "txtTXTtxt").
+ With("name", "sub").
+ With("type", "TXT").
+ With("ttl", "30"),
+ ).
+ Build(t)
record := Record{
Name: "sub",
@@ -101,9 +49,15 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/dns/example.com/addRR", testErrorHandler())
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded().
+ With(authenticationHeader, "user;1692475113;0123456789ABCDEF;24a32faf74c7bd0525f560ff12a1c1fb6545bafc"),
+ ).
+ Route("POST /dns/example.com/addRR",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
record := Record{
Name: "sub",
@@ -117,15 +71,18 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_RemoveRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- params := map[string]string{
- "data": "txtTXTtxt",
- "name": "sub",
- "type": "TXT",
- }
-
- mux.Handle("/dns/example.com/removeRR", testHandler(params))
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded().
+ With(authenticationHeader, "user;1692475113;0123456789ABCDEF;699f01f077ca487bd66ac370d6dfc5b122c65522"),
+ ).
+ Route("POST /dns/example.com/removeRR", nil,
+ servermock.CheckForm().Strict().
+ With("data", "txtTXTtxt").
+ With("name", "sub").
+ With("type", "TXT"),
+ ).
+ Build(t)
record := Record{
Name: "sub",
@@ -138,9 +95,15 @@ func TestClient_RemoveRecord(t *testing.T) {
}
func TestClient_RemoveRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.Handle("/dns/example.com/removeRR", testErrorHandler())
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded().
+ With(authenticationHeader, "user;1692475113;0123456789ABCDEF;699f01f077ca487bd66ac370d6dfc5b122c65522"),
+ ).
+ Route("POST /dns/example.com/removeRR",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
record := Record{
Name: "sub",
diff --git a/providers/dns/netcup/internal/client_live_test.go b/providers/dns/netcup/internal/client_live_test.go
new file mode 100644
index 000000000..3cf6c8c0b
--- /dev/null
+++ b/providers/dns/netcup/internal/client_live_test.go
@@ -0,0 +1,137 @@
+package internal
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/tester"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var envTest = tester.NewEnvTest(
+ "NETCUP_CUSTOMER_NUMBER",
+ "NETCUP_API_KEY",
+ "NETCUP_API_PASSWORD").
+ WithDomain("NETCUP_DOMAIN")
+
+func TestClient_GetDNSRecords_Live(t *testing.T) {
+ if !envTest.IsLiveTest() {
+ t.Skip("skipping live test")
+ }
+
+ // Setup
+ envTest.RestoreEnv()
+
+ client, err := NewClient(
+ envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
+ envTest.GetValue("NETCUP_API_KEY"),
+ envTest.GetValue("NETCUP_API_PASSWORD"))
+ require.NoError(t, err)
+
+ ctx, err := client.CreateSessionContext(t.Context())
+ require.NoError(t, err)
+
+ info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==")
+
+ zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ require.NoError(t, err, "error finding DNSZone")
+
+ zone = dns01.UnFqdn(zone)
+
+ // TestMethod
+ _, err = client.GetDNSRecords(ctx, zone)
+ require.NoError(t, err)
+
+ // Tear down
+ err = client.Logout(ctx)
+ require.NoError(t, err)
+}
+
+func TestClient_UpdateDNSRecord_Live(t *testing.T) {
+ if !envTest.IsLiveTest() {
+ t.Skip("skipping live test")
+ }
+
+ // Setup
+ envTest.RestoreEnv()
+
+ client, err := NewClient(
+ envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
+ envTest.GetValue("NETCUP_API_KEY"),
+ envTest.GetValue("NETCUP_API_PASSWORD"))
+ require.NoError(t, err)
+
+ ctx, err := client.CreateSessionContext(t.Context())
+ require.NoError(t, err)
+
+ info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==")
+
+ zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ require.NotErrorIs(t, err, fmt.Errorf("error finding DNSZone, %w", err))
+
+ hostname := strings.Replace(info.EffectiveFQDN, "."+zone, "", 1)
+
+ record := DNSRecord{
+ Hostname: hostname,
+ RecordType: "TXT",
+ Destination: "asdf5678",
+ DeleteRecord: false,
+ }
+
+ // test
+ zone = dns01.UnFqdn(zone)
+
+ err = client.UpdateDNSRecord(ctx, zone, []DNSRecord{record})
+ require.NoError(t, err)
+
+ records, err := client.GetDNSRecords(ctx, zone)
+ require.NoError(t, err)
+
+ recordIdx, err := GetDNSRecordIdx(records, record)
+ require.NoError(t, err)
+
+ assert.Equal(t, record.Hostname, records[recordIdx].Hostname)
+ assert.Equal(t, record.RecordType, records[recordIdx].RecordType)
+ assert.Equal(t, record.Destination, records[recordIdx].Destination)
+ assert.Equal(t, record.DeleteRecord, records[recordIdx].DeleteRecord)
+
+ records[recordIdx].DeleteRecord = true
+
+ // Tear down
+ err = client.UpdateDNSRecord(ctx, envTest.GetDomain(), []DNSRecord{records[recordIdx]})
+ require.NoError(t, err, "Did not remove record! Please do so yourself.")
+
+ err = client.Logout(ctx)
+ require.NoError(t, err)
+}
+
+func TestLiveClientAuth(t *testing.T) {
+ if !envTest.IsLiveTest() {
+ t.Skip("skipping live test")
+ }
+
+ // Setup
+ envTest.RestoreEnv()
+
+ client, err := NewClient(
+ envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
+ envTest.GetValue("NETCUP_API_KEY"),
+ envTest.GetValue("NETCUP_API_PASSWORD"))
+ require.NoError(t, err)
+
+ for i := range 4 {
+ t.Run("Test_"+strconv.Itoa(i+1), func(t *testing.T) {
+ t.Parallel()
+
+ ctx, err := client.CreateSessionContext(t.Context())
+ require.NoError(t, err)
+
+ err = client.Logout(ctx)
+ require.NoError(t, err)
+ })
+ }
+}
diff --git a/providers/dns/netcup/internal/client_test.go b/providers/dns/netcup/internal/client_test.go
index 501629e8f..a1c91aac4 100644
--- a/providers/dns/netcup/internal/client_test.go
+++ b/providers/dns/netcup/internal/client_test.go
@@ -1,40 +1,30 @@
package internal
import (
- "bytes"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
- "strings"
"testing"
- "github.com/go-acme/lego/v4/challenge/dns01"
- "github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-var envTest = tester.NewEnvTest(
- "NETCUP_CUSTOMER_NUMBER",
- "NETCUP_API_KEY",
- "NETCUP_API_PASSWORD").
- WithDomain("NETCUP_DOMAIN")
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("a", "b", "c")
+ if err != nil {
+ return nil, err
+ }
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+ client.baseURL = server.URL
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client, err := NewClient("a", "b", "c")
- require.NoError(t, err)
-
- client.baseURL = server.URL
- client.HTTPClient = server.Client()
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestGetDNSRecordIdx(t *testing.T) {
@@ -139,59 +129,10 @@ func TestGetDNSRecordIdx(t *testing.T) {
}
func TestClient_GetDNSRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- raw, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if string(bytes.TrimSpace(raw)) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":""}}` {
- http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
- return
- }
-
- response := `
- {
- "serverrequestid":"srv-request-id",
- "clientrequestid":"",
- "action":"infoDnsRecords",
- "status":"success",
- "statuscode":2000,
- "shortmessage":"Login successful",
- "longmessage":"Session has been created successful.",
- "responsedata":{
- "apisessionid":"api-session-id",
- "dnsrecords":[
- {
- "id":"1",
- "hostname":"example.com",
- "type":"TXT",
- "priority":"1",
- "destination":"bGVnbzE=",
- "state":"yes",
- "ttl":300
- },
- {
- "id":"2",
- "hostname":"example2.com",
- "type":"TXT",
- "priority":"1",
- "destination":"bGVnbw==",
- "state":"yes",
- "ttl":300
- }
- ]
- }
- }`
- _, err = rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("get_dns_records.json"),
+ servermock.CheckRequestJSONBodyFromFile("get_dns_records-request.json")).
+ Build(t)
expected := []DNSRecord{{
ID: 1,
@@ -219,67 +160,24 @@ func TestClient_GetDNSRecords(t *testing.T) {
func TestClient_GetDNSRecords_errors(t *testing.T) {
testCases := []struct {
- desc string
- handler func(rw http.ResponseWriter, req *http.Request)
+ desc string
+ handler http.Handler
+ expected string
}{
{
- desc: "HTTP error",
- handler: func(rw http.ResponseWriter, _ *http.Request) {
- http.Error(rw, "error message", http.StatusInternalServerError)
- },
+ desc: "HTTP error",
+ handler: servermock.Noop().WithStatusCode(http.StatusInternalServerError),
+ expected: `error when sending the request: unexpected status code: [status code: 500] body: `,
},
{
- desc: "API error",
- handler: func(rw http.ResponseWriter, _ *http.Request) {
- response := `
- {
- "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
- "clientrequestid":"",
- "action":"infoDnsRecords",
- "status":"error",
- "statuscode":4013,
- "shortmessage":"Validation Error.",
- "longmessage":"Message is empty.",
- "responsedata":""
- }`
- _, err := rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- },
+ desc: "API error",
+ handler: servermock.ResponseFromFixture("get_dns_records_error.json"),
+ expected: `error when sending the request: an error occurred during the action infoDnsRecords: [Status=error, StatusCode=4013, ShortMessage=Validation Error., LongMessage=Message is empty.]`,
},
{
- desc: "responsedata marshaling error",
- handler: func(rw http.ResponseWriter, req *http.Request) {
- raw, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if string(raw) != `{"action":"infoDnsRecords","param":{"domainname":"example.com","customernumber":"a","apikey":"b","apisessionid":"api-session-id"}}` {
- http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
- return
- }
-
- response := `
- {
- "serverrequestid":"srv-request-id",
- "clientrequestid":"",
- "action":"infoDnsRecords",
- "status":"success",
- "statuscode":2000,
- "shortmessage":"Login successful",
- "longmessage":"Session has been created successful.",
- "responsedata":""
- }`
- _, err = rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- },
+ desc: "responsedata marshaling error",
+ handler: servermock.ResponseFromFixture("get_dns_records_error_unmarshal.json"),
+ expected: `error when sending the request: unable to unmarshal response: [status code: 200] body: "" error: json: cannot unmarshal string into Go value of type internal.InfoDNSRecordsResponse`,
},
}
@@ -287,104 +185,13 @@ func TestClient_GetDNSRecords_errors(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", test.handler)
+ client := mockBuilder().
+ Route("POST /", test.handler).
+ Build(t)
records, err := client.GetDNSRecords(t.Context(), "example.com")
- require.Error(t, err)
+ require.EqualError(t, err, test.expected)
assert.Empty(t, records)
})
}
}
-
-func TestClient_GetDNSRecords_Live(t *testing.T) {
- if !envTest.IsLiveTest() {
- t.Skip("skipping live test")
- }
-
- // Setup
- envTest.RestoreEnv()
-
- client, err := NewClient(
- envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
- envTest.GetValue("NETCUP_API_KEY"),
- envTest.GetValue("NETCUP_API_PASSWORD"))
- require.NoError(t, err)
-
- ctx, err := client.CreateSessionContext(t.Context())
- require.NoError(t, err)
-
- info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==")
-
- zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
- require.NoError(t, err, "error finding DNSZone")
-
- zone = dns01.UnFqdn(zone)
-
- // TestMethod
- _, err = client.GetDNSRecords(ctx, zone)
- require.NoError(t, err)
-
- // Tear down
- err = client.Logout(ctx)
- require.NoError(t, err)
-}
-
-func TestClient_UpdateDNSRecord_Live(t *testing.T) {
- if !envTest.IsLiveTest() {
- t.Skip("skipping live test")
- }
-
- // Setup
- envTest.RestoreEnv()
-
- client, err := NewClient(
- envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
- envTest.GetValue("NETCUP_API_KEY"),
- envTest.GetValue("NETCUP_API_PASSWORD"))
- require.NoError(t, err)
-
- ctx, err := client.CreateSessionContext(t.Context())
- require.NoError(t, err)
-
- info := dns01.GetChallengeInfo(envTest.GetDomain(), "123d==")
-
- zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
- require.NotErrorIs(t, err, fmt.Errorf("error finding DNSZone, %w", err))
-
- hostname := strings.Replace(info.EffectiveFQDN, "."+zone, "", 1)
-
- record := DNSRecord{
- Hostname: hostname,
- RecordType: "TXT",
- Destination: "asdf5678",
- DeleteRecord: false,
- }
-
- // test
- zone = dns01.UnFqdn(zone)
-
- err = client.UpdateDNSRecord(ctx, zone, []DNSRecord{record})
- require.NoError(t, err)
-
- records, err := client.GetDNSRecords(ctx, zone)
- require.NoError(t, err)
-
- recordIdx, err := GetDNSRecordIdx(records, record)
- require.NoError(t, err)
-
- assert.Equal(t, record.Hostname, records[recordIdx].Hostname)
- assert.Equal(t, record.RecordType, records[recordIdx].RecordType)
- assert.Equal(t, record.Destination, records[recordIdx].Destination)
- assert.Equal(t, record.DeleteRecord, records[recordIdx].DeleteRecord)
-
- records[recordIdx].DeleteRecord = true
-
- // Tear down
- err = client.UpdateDNSRecord(ctx, envTest.GetDomain(), []DNSRecord{records[recordIdx]})
- require.NoError(t, err, "Did not remove record! Please do so yourself.")
-
- err = client.Logout(ctx)
- require.NoError(t, err)
-}
diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records-request.json b/providers/dns/netcup/internal/fixtures/get_dns_records-request.json
new file mode 100644
index 000000000..bcf8e5310
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/get_dns_records-request.json
@@ -0,0 +1,9 @@
+{
+ "action": "infoDnsRecords",
+ "param": {
+ "domainname": "example.com",
+ "customernumber": "a",
+ "apikey": "b",
+ "apisessionid": ""
+ }
+}
diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records.json b/providers/dns/netcup/internal/fixtures/get_dns_records.json
new file mode 100644
index 000000000..e521a8e24
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/get_dns_records.json
@@ -0,0 +1,32 @@
+{
+ "serverrequestid": "srv-request-id",
+ "clientrequestid": "",
+ "action": "infoDnsRecords",
+ "status": "success",
+ "statuscode": 2000,
+ "shortmessage": "Login successful",
+ "longmessage": "Session has been created successful.",
+ "responsedata": {
+ "apisessionid": "api-session-id",
+ "dnsrecords": [
+ {
+ "id": "1",
+ "hostname": "example.com",
+ "type": "TXT",
+ "priority": "1",
+ "destination": "bGVnbzE=",
+ "state": "yes",
+ "ttl": 300
+ },
+ {
+ "id": "2",
+ "hostname": "example2.com",
+ "type": "TXT",
+ "priority": "1",
+ "destination": "bGVnbw==",
+ "state": "yes",
+ "ttl": 300
+ }
+ ]
+ }
+}
diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records_error.json b/providers/dns/netcup/internal/fixtures/get_dns_records_error.json
new file mode 100644
index 000000000..3ba472366
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/get_dns_records_error.json
@@ -0,0 +1,10 @@
+{
+ "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
+ "clientrequestid":"",
+ "action":"infoDnsRecords",
+ "status":"error",
+ "statuscode":4013,
+ "shortmessage":"Validation Error.",
+ "longmessage":"Message is empty.",
+ "responsedata":""
+}
diff --git a/providers/dns/netcup/internal/fixtures/get_dns_records_error_unmarshal.json b/providers/dns/netcup/internal/fixtures/get_dns_records_error_unmarshal.json
new file mode 100644
index 000000000..f8f91329f
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/get_dns_records_error_unmarshal.json
@@ -0,0 +1,10 @@
+{
+ "serverrequestid":"srv-request-id",
+ "clientrequestid":"",
+ "action":"infoDnsRecords",
+ "status":"success",
+ "statuscode":2000,
+ "shortmessage":"Login successful",
+ "longmessage":"Session has been created successful.",
+ "responsedata":""
+}
diff --git a/providers/dns/netcup/internal/fixtures/login-request.json b/providers/dns/netcup/internal/fixtures/login-request.json
new file mode 100644
index 000000000..1e287dfe0
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/login-request.json
@@ -0,0 +1,8 @@
+{
+ "action": "login",
+ "param": {
+ "customernumber": "a",
+ "apikey": "b",
+ "apipassword": "c"
+ }
+}
diff --git a/providers/dns/netcup/internal/fixtures/login.json b/providers/dns/netcup/internal/fixtures/login.json
new file mode 100644
index 000000000..a66979544
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/login.json
@@ -0,0 +1,12 @@
+{
+ "serverrequestid": "srv-request-id",
+ "clientrequestid": "",
+ "action": "login",
+ "status": "success",
+ "statuscode": 2000,
+ "shortmessage": "Login successful",
+ "longmessage": "Session has been created successful.",
+ "responsedata": {
+ "apisessionid": "api-session-id"
+ }
+}
diff --git a/providers/dns/netcup/internal/fixtures/login_error.json b/providers/dns/netcup/internal/fixtures/login_error.json
new file mode 100644
index 000000000..a32568f78
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/login_error.json
@@ -0,0 +1,10 @@
+{
+ "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
+ "clientrequestid":"",
+ "action":"login",
+ "status":"error",
+ "statuscode":4013,
+ "shortmessage":"Validation Error.",
+ "longmessage":"Message is empty.",
+ "responsedata":""
+}
diff --git a/providers/dns/netcup/internal/fixtures/login_error_unmarshal.json b/providers/dns/netcup/internal/fixtures/login_error_unmarshal.json
new file mode 100644
index 000000000..96e7cbd0c
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/login_error_unmarshal.json
@@ -0,0 +1,10 @@
+{
+ "serverrequestid": "srv-request-id",
+ "clientrequestid": "",
+ "action": "login",
+ "status": "success",
+ "statuscode": 2000,
+ "shortmessage": "Login successful",
+ "longmessage": "Session has been created successful.",
+ "responsedata": ""
+}
diff --git a/providers/dns/netcup/internal/fixtures/logout-request.json b/providers/dns/netcup/internal/fixtures/logout-request.json
new file mode 100644
index 000000000..add759c3a
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/logout-request.json
@@ -0,0 +1,8 @@
+{
+ "action": "logout",
+ "param": {
+ "customernumber": "a",
+ "apikey": "b",
+ "apisessionid": "session-id"
+ }
+}
diff --git a/providers/dns/netcup/internal/fixtures/logout.json b/providers/dns/netcup/internal/fixtures/logout.json
new file mode 100644
index 000000000..50881fff3
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/logout.json
@@ -0,0 +1,10 @@
+{
+ "serverrequestid": "request-id",
+ "clientrequestid": "",
+ "action": "logout",
+ "status": "success",
+ "statuscode": 2000,
+ "shortmessage": "Logout successful",
+ "longmessage": "Session has been terminated successful.",
+ "responsedata": ""
+}
diff --git a/providers/dns/netcup/internal/fixtures/logout_error.json b/providers/dns/netcup/internal/fixtures/logout_error.json
new file mode 100644
index 000000000..a2de32da1
--- /dev/null
+++ b/providers/dns/netcup/internal/fixtures/logout_error.json
@@ -0,0 +1,10 @@
+{
+ "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
+ "clientrequestid":"",
+ "action":"logout",
+ "status":"error",
+ "statuscode":4013,
+ "shortmessage":"Validation Error.",
+ "longmessage":"Message is empty.",
+ "responsedata":""
+}
diff --git a/providers/dns/netcup/internal/session_test.go b/providers/dns/netcup/internal/session_test.go
index ceec56708..27442b347 100644
--- a/providers/dns/netcup/internal/session_test.go
+++ b/providers/dns/netcup/internal/session_test.go
@@ -1,14 +1,11 @@
package internal
import (
- "bytes"
"context"
- "fmt"
- "io"
"net/http"
- "strconv"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -20,40 +17,10 @@ func mockContext(t *testing.T) context.Context {
}
func TestClient_Login(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- raw, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if string(bytes.TrimSpace(raw)) != `{"action":"login","param":{"customernumber":"a","apikey":"b","apipassword":"c"}}` {
- http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
- return
- }
-
- response := `
- {
- "serverrequestid": "srv-request-id",
- "clientrequestid": "",
- "action": "login",
- "status": "success",
- "statuscode": 2000,
- "shortmessage": "Login successful",
- "longmessage": "Session has been created successful.",
- "responsedata": {
- "apisessionid": "api-session-id"
- }
- }
- `
- _, err = rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("login.json"),
+ servermock.CheckRequestJSONBodyFromFile("login-request.json")).
+ Build(t)
sessionID, err := client.login(t.Context())
require.NoError(t, err)
@@ -63,56 +30,24 @@ func TestClient_Login(t *testing.T) {
func TestClient_Login_errors(t *testing.T) {
testCases := []struct {
- desc string
- handler func(rw http.ResponseWriter, req *http.Request)
+ desc string
+ handler http.Handler
+ expected string
}{
{
- desc: "HTTP error",
- handler: func(rw http.ResponseWriter, _ *http.Request) {
- http.Error(rw, "error message", http.StatusInternalServerError)
- },
+ desc: "HTTP error",
+ handler: servermock.Noop().WithStatusCode(http.StatusInternalServerError),
+ expected: `loging error: unexpected status code: [status code: 500] body: `,
},
{
- desc: "API error",
- handler: func(rw http.ResponseWriter, _ *http.Request) {
- response := `
- {
- "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
- "clientrequestid":"",
- "action":"login",
- "status":"error",
- "statuscode":4013,
- "shortmessage":"Validation Error.",
- "longmessage":"Message is empty.",
- "responsedata":""
- }`
- _, err := rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- },
+ desc: "API error",
+ handler: servermock.ResponseFromFixture("login_error.json"),
+ expected: `loging error: an error occurred during the action login: [Status=error, StatusCode=4013, ShortMessage=Validation Error., LongMessage=Message is empty.]`,
},
{
- desc: "responsedata marshaling error",
- handler: func(rw http.ResponseWriter, _ *http.Request) {
- response := `
- {
- "serverrequestid": "srv-request-id",
- "clientrequestid": "",
- "action": "login",
- "status": "success",
- "statuscode": 2000,
- "shortmessage": "Login successful",
- "longmessage": "Session has been created successful.",
- "responsedata": ""
- }`
- _, err := rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- },
+ desc: "responsedata marshaling error",
+ handler: servermock.ResponseFromFixture("login_error_unmarshal.json"),
+ expected: `loging error: unable to unmarshal response: [status code: 200] body: "" error: json: cannot unmarshal string into Go value of type internal.LoginResponse`,
},
}
@@ -120,49 +55,22 @@ func TestClient_Login_errors(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", test.handler)
+ client := mockBuilder().
+ Route("POST /", test.handler).
+ Build(t)
sessionID, err := client.login(t.Context())
- assert.Error(t, err)
+ assert.EqualError(t, err, test.expected)
assert.Empty(t, sessionID)
})
}
}
func TestClient_Logout(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- raw, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if string(bytes.TrimSpace(raw)) != `{"action":"logout","param":{"customernumber":"a","apikey":"b","apisessionid":"session-id"}}` {
- http.Error(rw, fmt.Sprintf("invalid request body: %s", string(raw)), http.StatusBadRequest)
- return
- }
-
- response := `
- {
- "serverrequestid": "request-id",
- "clientrequestid": "",
- "action": "logout",
- "status": "success",
- "statuscode": 2000,
- "shortmessage": "Logout successful",
- "longmessage": "Session has been terminated successful.",
- "responsedata": ""
- }`
- _, err = rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /", servermock.ResponseFromFixture("logout.json"),
+ servermock.CheckRequestJSONBodyFromFile("logout-request.json")).
+ Build(t)
err := client.Logout(mockContext(t))
require.NoError(t, err)
@@ -170,35 +78,17 @@ func TestClient_Logout(t *testing.T) {
func TestClient_Logout_errors(t *testing.T) {
testCases := []struct {
- desc string
- handler func(rw http.ResponseWriter, req *http.Request)
+ desc string
+ handler http.Handler
+ expected string
}{
{
- desc: "HTTP error",
- handler: func(rw http.ResponseWriter, _ *http.Request) {
- http.Error(rw, "error message", http.StatusInternalServerError)
- },
+ desc: "HTTP error",
+ handler: servermock.Noop().WithStatusCode(http.StatusInternalServerError),
},
{
- desc: "API error",
- handler: func(rw http.ResponseWriter, _ *http.Request) {
- response := `
- {
- "serverrequestid":"YxTr4EzdbJ101T211zR4yzUEMVE",
- "clientrequestid":"",
- "action":"logout",
- "status":"error",
- "statuscode":4013,
- "shortmessage":"Validation Error.",
- "longmessage":"Message is empty.",
- "responsedata":""
- }`
- _, err := rw.Write([]byte(response))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- },
+ desc: "API error",
+ handler: servermock.ResponseFromFixture("login_error.json"),
},
}
@@ -206,39 +96,12 @@ func TestClient_Logout_errors(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client, mux := setupTest(t)
-
- mux.HandleFunc("/", test.handler)
+ client := mockBuilder().
+ Route("POST /", test.handler).
+ Build(t)
err := client.Logout(t.Context())
require.Error(t, err)
})
}
}
-
-func TestLiveClientAuth(t *testing.T) {
- if !envTest.IsLiveTest() {
- t.Skip("skipping live test")
- }
-
- // Setup
- envTest.RestoreEnv()
-
- client, err := NewClient(
- envTest.GetValue("NETCUP_CUSTOMER_NUMBER"),
- envTest.GetValue("NETCUP_API_KEY"),
- envTest.GetValue("NETCUP_API_PASSWORD"))
- require.NoError(t, err)
-
- for i := range 4 {
- t.Run("Test_"+strconv.Itoa(i+1), func(t *testing.T) {
- t.Parallel()
-
- ctx, err := client.CreateSessionContext(t.Context())
- require.NoError(t, err)
-
- err = client.Logout(ctx)
- require.NoError(t, err)
- })
- }
-}
diff --git a/providers/dns/netlify/internal/client_test.go b/providers/dns/netlify/internal/client_test.go
index a1e9e09a3..b19a8f071 100644
--- a/providers/dns/netlify/internal/client_test.go
+++ b/providers/dns/netlify/internal/client_test.go
@@ -1,61 +1,33 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, token string) (*Client, *http.ServeMux) {
- t.Helper()
+func setupClient(token string) func(server *httptest.Server) (*Client, error) {
+ return func(server *httptest.Server) (*Client, error) {
+ client := NewClient(OAuthStaticAccessToken(server.Client(), token))
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(OAuthStaticAccessToken(server.Client(), token))
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ }
}
func TestClient_GetRecords(t *testing.T) {
- client, mux := setupTest(t, "tokenA")
-
- mux.HandleFunc("/dns_zones/zoneID/dns_records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Bearer tokenA" {
- http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized)
- return
- }
-
- rw.Header().Set("Content-Type", "application/json; charset=utf-8")
-
- file, err := os.Open("./fixtures/get_records.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient("tokenA"),
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer tokenA"),
+ ).
+ Route("GET /dns_zones/zoneID/dns_records",
+ servermock.ResponseFromFixture("get_records.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "zoneID")
require.NoError(t, err)
@@ -69,36 +41,16 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_CreateRecord(t *testing.T) {
- client, mux := setupTest(t, "tokenB")
-
- mux.HandleFunc("/dns_zones/zoneID/dns_records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Bearer tokenB" {
- http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized)
- return
- }
-
- rw.Header().Set("Content-Type", "application/json; charset=utf-8")
- rw.WriteHeader(http.StatusCreated)
-
- file, err := os.Open("./fixtures/create_record.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient("tokenB"),
+ servermock.CheckHeader().
+ WithAccept("application/json").
+ WithContentType("application/json; charset=utf-8").
+ WithAuthorization("Bearer tokenB"),
+ ).
+ Route("POST /dns_zones/zoneID/dns_records",
+ servermock.ResponseFromFixture("create_record.json").
+ WithStatusCode(http.StatusCreated)).
+ Build(t)
record := DNSRecord{
Hostname: "_acme-challenge.example.com",
@@ -122,22 +74,14 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_RemoveRecord(t *testing.T) {
- client, mux := setupTest(t, "tokenC")
-
- mux.HandleFunc("/dns_zones/zoneID/dns_records/recordID", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Bearer tokenC" {
- http.Error(rw, fmt.Sprintf("invali token: %s", auth), http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(http.StatusNoContent)
- })
+ client := servermock.NewBuilder[*Client](setupClient("tokenC"),
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer tokenC"),
+ ).
+ Route("DELETE /dns_zones/zoneID/dns_records/recordID",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.RemoveRecord(t.Context(), "zoneID", "recordID")
require.NoError(t, err)
diff --git a/providers/dns/nicmanager/internal/client_test.go b/providers/dns/nicmanager/internal/client_test.go
index 9c8679bea..1eb7d5a36 100644
--- a/providers/dns/nicmanager/internal/client_test.go
+++ b/providers/dns/nicmanager/internal/client_test.go
@@ -1,21 +1,42 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ opts := Options{
+ Login: "l",
+ Username: "u",
+ Password: "p",
+ OTP: "2hsn",
+ }
+
+ client := NewClient(opts)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("l.u", "p").
+ WithRegexp(headerTOTPToken, `\d{6}`))
+}
+
func TestClient_GetZone(t *testing.T) {
- client := setupTest(t, "/anycast/nicmanager-anycastdns4.net", testHandler(http.MethodGet, http.StatusOK, "zone.json"))
+ client := mockBuilder().
+ Route("GET /anycast/nicmanager-anycastdns4.net",
+ servermock.ResponseFromFixture("zone.json")).
+ Build(t)
zone, err := client.GetZone(t.Context(), "nicmanager-anycastdns4.net")
require.NoError(t, err)
@@ -38,14 +59,22 @@ func TestClient_GetZone(t *testing.T) {
}
func TestClient_GetZone_error(t *testing.T) {
- client := setupTest(t, "/anycast/foo", testHandler(http.MethodGet, http.StatusNotFound, "error.json"))
+ client := mockBuilder().
+ Route("GET /anycast/foo",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
_, err := client.GetZone(t.Context(), "foo")
- require.Error(t, err)
+ require.EqualError(t, err, "404: Not Found")
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/anycast/zonedomain.tld/records", testHandler(http.MethodPost, http.StatusAccepted, "error.json"))
+ client := mockBuilder().
+ Route("POST /anycast/zonedomain.tld/records",
+ servermock.Noop().
+ WithStatusCode(http.StatusAccepted)).
+ Build(t)
record := RecordCreateUpdate{
Type: "TXT",
@@ -59,7 +88,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "/anycast/zonedomain.tld", testHandler(http.MethodPost, http.StatusUnauthorized, "error.json"))
+ client := mockBuilder().
+ Route("POST /anycast/zonedomain.tld/records",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
record := RecordCreateUpdate{
Type: "TXT",
@@ -69,77 +102,27 @@ func TestClient_AddRecord_error(t *testing.T) {
}
err := client.AddRecord(t.Context(), "zonedomain.tld", record)
- require.Error(t, err)
+ require.EqualError(t, err, "401: Not Found")
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusAccepted, "error.json"))
+ client := mockBuilder().
+ Route("DELETE /anycast/zonedomain.tld/records/6",
+ servermock.Noop().
+ WithStatusCode(http.StatusAccepted)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "zonedomain.tld", 6)
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "/anycast/zonedomain.tld/records/6", testHandler(http.MethodDelete, http.StatusNoContent, ""))
+ client := mockBuilder().
+ Route("DELETE /anycast/zonedomain.tld/records/6",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
- err := client.DeleteRecord(t.Context(), "zonedomain.tld", 7)
- require.Error(t, err)
-}
-
-func setupTest(t *testing.T, path string, handler http.Handler) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.Handle(path, handler)
-
- opts := Options{
- Login: "foo",
- Username: "bar",
- Password: "foo",
- OTP: "2hsn",
- }
-
- client := NewClient(opts)
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
-}
-
-func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- username, password, ok := req.BasicAuth()
- if !ok || username != "foo.bar" || password != "foo" {
- http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(statusCode)
-
- if statusCode == http.StatusNoContent {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
- return
- }
- }
+ err := client.DeleteRecord(t.Context(), "zonedomain.tld", 6)
+ require.EqualError(t, err, "404: Not Found")
}
diff --git a/providers/dns/nicru/internal/client_test.go b/providers/dns/nicru/internal/client_test.go
index d49aa4014..f01300406 100644
--- a/providers/dns/nicru/internal/client_test.go
+++ b/providers/dns/nicru/internal/client_test.go
@@ -1,63 +1,36 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient(server.Client())
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, handler)
-
- client, err := NewClient(server.Client())
- require.NoError(t, err)
-
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
-}
-
-func writeFixtures(method, filename string, status int) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithAccept("text/xml"),
+ )
}
func TestClient_GetServices(t *testing.T) {
- client := setupTest(t, "/services",
- writeFixtures(http.MethodGet, "services_GET.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("GET /services", servermock.ResponseFromFixture("services_GET.xml")).
+ Build(t)
zones, err := client.GetServices(t.Context())
require.NoError(t, err)
@@ -91,8 +64,9 @@ func TestClient_GetServices(t *testing.T) {
}
func TestClient_ListZones(t *testing.T) {
- client := setupTest(t, "/zones",
- writeFixtures(http.MethodGet, "zones_all_GET.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("GET /zones", servermock.ResponseFromFixture("zones_all_GET.xml")).
+ Build(t)
zones, err := client.ListZones(t.Context())
require.NoError(t, err)
@@ -137,8 +111,9 @@ func TestClient_ListZones(t *testing.T) {
}
func TestClient_ListZones_error(t *testing.T) {
- client := setupTest(t, "/zones",
- writeFixtures(http.MethodGet, "errors.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("GET /zones", servermock.ResponseFromFixture("errors.xml")).
+ Build(t)
_, err := client.ListZones(t.Context())
require.ErrorIs(t, err, Error{
@@ -148,8 +123,10 @@ func TestClient_ListZones_error(t *testing.T) {
}
func TestClient_GetZonesByService(t *testing.T) {
- client := setupTest(t, "/services/test/zones",
- writeFixtures(http.MethodGet, "zones_GET.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("GET /services/test/zones",
+ servermock.ResponseFromFixture("zones_GET.xml")).
+ Build(t)
zones, err := client.GetZonesByService(t.Context(), "test")
require.NoError(t, err)
@@ -194,8 +171,10 @@ func TestClient_GetZonesByService(t *testing.T) {
}
func TestClient_GetZonesByService_error(t *testing.T) {
- client := setupTest(t, "/services/test/zones",
- writeFixtures(http.MethodGet, "errors.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("GET /services/test/zones",
+ servermock.ResponseFromFixture("errors.xml")).
+ Build(t)
_, err := client.GetZonesByService(t.Context(), "test")
require.ErrorIs(t, err, Error{
@@ -205,8 +184,10 @@ func TestClient_GetZonesByService_error(t *testing.T) {
}
func TestClient_GetRecords(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./records",
- writeFixtures(http.MethodGet, "records_GET.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("GET /services/test/zones/example.com./records",
+ servermock.ResponseFromFixture("records_GET.xml")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "test", "example.com.")
require.NoError(t, err)
@@ -270,8 +251,10 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_error(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./records",
- writeFixtures(http.MethodGet, "errors.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("GET /services/test/zones/example.com./records",
+ servermock.ResponseFromFixture("errors.xml")).
+ Build(t)
_, err := client.GetRecords(t.Context(), "test", "example.com.")
require.ErrorIs(t, err, Error{
@@ -281,8 +264,12 @@ func TestClient_GetRecords_error(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./records",
- writeFixtures(http.MethodPut, "records_PUT.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("PUT /services/test/zones/example.com./records",
+ servermock.ResponseFromFixture("records_PUT.xml"),
+ servermock.CheckHeader().
+ WithContentType("text/xml")).
+ Build(t)
rrs := []RR{
{
@@ -337,8 +324,12 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./records",
- writeFixtures(http.MethodPut, "errors.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("PUT /services/test/zones/example.com./records",
+ servermock.ResponseFromFixture("errors.xml"),
+ servermock.CheckHeader().
+ WithContentType("text/xml")).
+ Build(t)
rrs := []RR{
{
@@ -361,16 +352,20 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./records/123",
- writeFixtures(http.MethodDelete, "record_DELETE.xml", http.StatusUnauthorized))
+ client := mockBuilder().
+ Route("DELETE /services/test/zones/example.com./records/123",
+ servermock.ResponseFromFixture("record_DELETE.xml")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "test", "example.com.", "123")
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./records/123",
- writeFixtures(http.MethodDelete, "errors.xml", http.StatusUnauthorized))
+ client := mockBuilder().
+ Route("DELETE /services/test/zones/example.com./records/123",
+ servermock.ResponseFromFixture("errors.xml")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "test", "example.com.", "123")
require.ErrorIs(t, err, Error{
@@ -380,14 +375,20 @@ func TestClient_DeleteRecord_error(t *testing.T) {
}
func TestClient_CommitZone(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./commit", writeFixtures(http.MethodPost, "commit_POST.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("POST /services/test/zones/example.com./commit",
+ servermock.ResponseFromFixture("commit_POST.xml")).
+ Build(t)
err := client.CommitZone(t.Context(), "test", "example.com.")
require.NoError(t, err)
}
func TestClient_CommitZone_error(t *testing.T) {
- client := setupTest(t, "/services/test/zones/example.com./commit", writeFixtures(http.MethodPost, "errors.xml", http.StatusOK))
+ client := mockBuilder().
+ Route("POST /services/test/zones/example.com./commit",
+ servermock.ResponseFromFixture("errors.xml")).
+ Build(t)
err := client.CommitZone(t.Context(), "test", "example.com.")
require.ErrorIs(t, err, Error{
diff --git a/providers/dns/nifcloud/internal/client_test.go b/providers/dns/nifcloud/internal/client_test.go
index 91f9d36e2..501265ada 100644
--- a/providers/dns/nifcloud/internal/client_test.go
+++ b/providers/dns/nifcloud/internal/client_test.go
@@ -1,37 +1,35 @@
package internal
import (
- "fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, responseBody string, statusCode int) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("A", "B")
+ if err != nil {
+ return nil, err
+ }
- handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(statusCode)
- _, _ = fmt.Fprintln(w, responseBody)
- })
+ client.HTTPClient = server.Client()
+ client.BaseURL, _ = url.Parse(server.URL)
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- client, err := NewClient("A", "B")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.BaseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithRegexp("X-Nifty-Authorization", "NIFTY3-HTTPS NiftyAccessKeyId=A,Algorithm=HmacSHA1,Signature=.+"),
+ )
}
-func TestChangeResourceRecordSets(t *testing.T) {
+func TestClient_ChangeResourceRecordSets(t *testing.T) {
responseBody := `
@@ -42,7 +40,10 @@ func TestChangeResourceRecordSets(t *testing.T) {
`
- client := setupTest(t, responseBody, http.StatusOK)
+ client := mockBuilder().
+ Route("POST /", servermock.RawStringResponse(responseBody),
+ servermock.CheckHeader().WithContentType("text/xml; charset=utf-8")).
+ Build(t)
res, err := client.ChangeResourceRecordSets(t.Context(), "example.com", ChangeResourceRecordSetsRequest{})
require.NoError(t, err)
@@ -52,7 +53,7 @@ func TestChangeResourceRecordSets(t *testing.T) {
assert.Equal(t, "2015-08-05T00:00:00.000Z", res.ChangeInfo.SubmittedAt)
}
-func TestChangeResourceRecordSetsErrors(t *testing.T) {
+func TestClient_ChangeResourceRecordSets_errors(t *testing.T) {
testCases := []struct {
desc string
responseBody string
@@ -89,7 +90,13 @@ func TestChangeResourceRecordSetsErrors(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := setupTest(t, test.responseBody, test.statusCode)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.RawStringResponse(test.responseBody).
+ WithStatusCode(test.statusCode),
+ servermock.CheckHeader().
+ WithContentType("text/xml; charset=utf-8")).
+ Build(t)
res, err := client.ChangeResourceRecordSets(t.Context(), "example.com", ChangeResourceRecordSetsRequest{})
assert.Nil(t, res)
@@ -98,7 +105,7 @@ func TestChangeResourceRecordSetsErrors(t *testing.T) {
}
}
-func TestGetChange(t *testing.T) {
+func TestClient_GetChange(t *testing.T) {
responseBody := `
@@ -109,7 +116,9 @@ func TestGetChange(t *testing.T) {
`
- client := setupTest(t, responseBody, http.StatusOK)
+ client := mockBuilder().
+ Route("GET /", servermock.RawStringResponse(responseBody)).
+ Build(t)
res, err := client.GetChange(t.Context(), "12345")
require.NoError(t, err)
@@ -119,7 +128,7 @@ func TestGetChange(t *testing.T) {
assert.Equal(t, "2015-08-05T00:00:00.000Z", res.ChangeInfo.SubmittedAt)
}
-func TestGetChangeErrors(t *testing.T) {
+func TestClient_GetChange_errors(t *testing.T) {
testCases := []struct {
desc string
responseBody string
@@ -156,7 +165,10 @@ func TestGetChangeErrors(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- client := setupTest(t, test.responseBody, test.statusCode)
+ client := mockBuilder().
+ Route("GET /",
+ servermock.RawStringResponse(test.responseBody).WithStatusCode(test.statusCode)).
+ Build(t)
res, err := client.GetChange(t.Context(), "12345")
assert.Nil(t, res)
diff --git a/providers/dns/njalla/internal/client_test.go b/providers/dns/njalla/internal/client_test.go
index 9ad58f24b..ec9309078 100644
--- a/providers/dns/njalla/internal/client_test.go
+++ b/providers/dns/njalla/internal/client_test.go
@@ -1,75 +1,31 @@
package internal
import (
- "encoding/json"
- "fmt"
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, handler func(http.ResponseWriter, *http.Request)) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- token := req.Header.Get(authorizationHeader)
- if token != "Njalla secret" {
- _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 403, "message": "Invalid token."}}`))
- return
- }
-
- if handler != nil {
- handler(rw, req)
- } else {
- _, _ = rw.Write([]byte(`{"jsonrpc":"2.0"}`))
- }
- })
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("secret")
client.apiEndpoint = server.URL
+ client.HTTPClient = server.Client()
- return client
+ return client, nil
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
- apiReq := struct {
- Method string `json:"method"`
- Params Record `json:"params"`
- }{}
-
- err := json.NewDecoder(req.Body).Decode(&apiReq)
- if err != nil {
- http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError)
- return
- }
-
- apiReq.Params.ID = "123"
-
- resp := map[string]any{
- "jsonrpc": "2.0",
- "id": "897",
- "result": apiReq.Params,
- }
-
- err = json.NewEncoder(rw).Encode(resp)
- if err != nil {
- http.Error(rw, "failed to marshal test response", http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Njalla secret"),
+ ).
+ Route("POST /",
+ servermock.ResponseFromFixture("add_record.json"),
+ servermock.CheckRequestJSONBodyFromFile("add_record-request.json")).
+ Build(t)
record := Record{
Content: "foobar",
@@ -94,7 +50,13 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, nil)
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Njalla invalid"),
+ ).
+ Route("POST /", servermock.ResponseFromFixture("auth_error.json")).
+ Build(t)
+
client.token = "invalid"
record := Record{
@@ -106,55 +68,20 @@ func TestClient_AddRecord_error(t *testing.T) {
}
result, err := client.AddRecord(t.Context(), record)
- require.Error(t, err)
+ require.EqualError(t, err, "code: 403, message: Invalid token.")
assert.Nil(t, result)
}
func TestClient_ListRecords(t *testing.T) {
- client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
- apiReq := struct {
- Method string `json:"method"`
- Params Record `json:"params"`
- }{}
-
- err := json.NewDecoder(req.Body).Decode(&apiReq)
- if err != nil {
- http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError)
- return
- }
-
- resp := map[string]any{
- "jsonrpc": "2.0",
- "id": "897",
- "result": Records{
- Records: []Record{
- {
- ID: "1",
- Domain: apiReq.Params.Domain,
- Content: "test",
- Name: "test01",
- TTL: 300,
- Type: "TXT",
- },
- {
- ID: "2",
- Domain: apiReq.Params.Domain,
- Content: "txtTxt",
- Name: "test02",
- TTL: 120,
- Type: "TXT",
- },
- },
- },
- }
-
- err = json.NewEncoder(rw).Encode(resp)
- if err != nil {
- http.Error(rw, "failed to marshal test response", http.StatusInternalServerError)
- return
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Njalla secret"),
+ ).
+ Route("POST /",
+ servermock.ResponseFromFixture("list_records.json"),
+ servermock.CheckRequestJSONBodyFromFile("list_records-request.json")).
+ Build(t)
records, err := client.ListRecords(t.Context(), "example.com")
require.NoError(t, err)
@@ -182,49 +109,43 @@ func TestClient_ListRecords(t *testing.T) {
}
func TestClient_ListRecords_error(t *testing.T) {
- client := setupTest(t, nil)
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Njalla invalid"),
+ ).
+ Route("POST /", servermock.ResponseFromFixture("auth_error.json")).
+ Build(t)
+
client.token = "invalid"
records, err := client.ListRecords(t.Context(), "example.com")
- require.Error(t, err)
+ require.EqualError(t, err, "code: 403, message: Invalid token.")
assert.Empty(t, records)
}
func TestClient_RemoveRecord(t *testing.T) {
- client := setupTest(t, func(rw http.ResponseWriter, req *http.Request) {
- apiReq := struct {
- Method string `json:"method"`
- Params Record `json:"params"`
- }{}
-
- err := json.NewDecoder(req.Body).Decode(&apiReq)
- if err != nil {
- http.Error(rw, "failed to marshal test request body", http.StatusInternalServerError)
- return
- }
-
- if apiReq.Params.ID == "" {
- _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 400, "message": ""missing ID"}}`))
- return
- }
-
- if apiReq.Params.Domain == "" {
- _, _ = rw.Write([]byte(`{"jsonrpc":"2.0", "Error": {"code": 400, "message": ""missing domain"}}`))
- return
- }
-
- _, _ = rw.Write([]byte(`{"jsonrpc":"2.0"}`))
- })
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Njalla secret"),
+ ).
+ Route("POST /",
+ servermock.RawStringResponse(`{"jsonrpc":"2.0"}`),
+ servermock.CheckRequestJSONBodyFromFile("remove_record-request.json")).
+ Build(t)
err := client.RemoveRecord(t.Context(), "123", "example.com")
require.NoError(t, err)
}
func TestClient_RemoveRecord_error(t *testing.T) {
- client := setupTest(t, nil)
- client.token = "invalid"
+ client := servermock.NewBuilder[*Client](setupClient,
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Njalla secret"),
+ ).
+ Route("POST /", servermock.ResponseFromFixture("remove_record_error_missing_domain.json")).
+ Build(t)
err := client.RemoveRecord(t.Context(), "123", "example.com")
- require.Error(t, err)
+ require.EqualError(t, err, "code: 400, message: missing domain")
}
diff --git a/providers/dns/njalla/internal/fixtures/add_record-request.json b/providers/dns/njalla/internal/fixtures/add_record-request.json
new file mode 100644
index 000000000..a85e1aaf1
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/add_record-request.json
@@ -0,0 +1,10 @@
+{
+ "method": "add-record",
+ "params": {
+ "content": "foobar",
+ "domain": "test",
+ "name": "example.com",
+ "ttl": 300,
+ "type": "TXT"
+ }
+}
diff --git a/providers/dns/njalla/internal/fixtures/add_record.json b/providers/dns/njalla/internal/fixtures/add_record.json
new file mode 100644
index 000000000..a537762bf
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/add_record.json
@@ -0,0 +1,12 @@
+{
+ "id": "897",
+ "jsonrpc": "2.0",
+ "result": {
+ "id": "123",
+ "content": "foobar",
+ "domain": "test",
+ "name": "example.com",
+ "ttl": 300,
+ "type": "TXT"
+ }
+}
diff --git a/providers/dns/njalla/internal/fixtures/auth_error.json b/providers/dns/njalla/internal/fixtures/auth_error.json
new file mode 100644
index 000000000..e9d07be51
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/auth_error.json
@@ -0,0 +1,7 @@
+{
+ "jsonrpc": "2.0",
+ "Error": {
+ "code": 403,
+ "message": "Invalid token."
+ }
+}
diff --git a/providers/dns/njalla/internal/fixtures/list_records-request.json b/providers/dns/njalla/internal/fixtures/list_records-request.json
new file mode 100644
index 000000000..ebe5ccf72
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/list_records-request.json
@@ -0,0 +1,6 @@
+{
+ "method": "list-records",
+ "params": {
+ "domain": "example.com"
+ }
+}
diff --git a/providers/dns/njalla/internal/fixtures/list_records.json b/providers/dns/njalla/internal/fixtures/list_records.json
new file mode 100644
index 000000000..a280a4b3f
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/list_records.json
@@ -0,0 +1,24 @@
+{
+ "id": "897",
+ "jsonrpc": "2.0",
+ "result": {
+ "records": [
+ {
+ "id": "1",
+ "content": "test",
+ "domain": "example.com",
+ "name": "test01",
+ "ttl": 300,
+ "type": "TXT"
+ },
+ {
+ "id": "2",
+ "content": "txtTxt",
+ "domain": "example.com",
+ "name": "test02",
+ "ttl": 120,
+ "type": "TXT"
+ }
+ ]
+ }
+}
diff --git a/providers/dns/njalla/internal/fixtures/remove_record-request.json b/providers/dns/njalla/internal/fixtures/remove_record-request.json
new file mode 100644
index 000000000..c96e94423
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/remove_record-request.json
@@ -0,0 +1,7 @@
+{
+ "method": "remove-record",
+ "params": {
+ "id": "123",
+ "domain": "example.com"
+ }
+}
diff --git a/providers/dns/njalla/internal/fixtures/remove_record_error_missing_domain.json b/providers/dns/njalla/internal/fixtures/remove_record_error_missing_domain.json
new file mode 100644
index 000000000..f65d254d0
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/remove_record_error_missing_domain.json
@@ -0,0 +1,7 @@
+{
+ "jsonrpc": "2.0",
+ "Error": {
+ "code": 400,
+ "message": "missing domain"
+ }
+}
diff --git a/providers/dns/njalla/internal/fixtures/remove_record_error_missing_id.json b/providers/dns/njalla/internal/fixtures/remove_record_error_missing_id.json
new file mode 100644
index 000000000..544cd4d1c
--- /dev/null
+++ b/providers/dns/njalla/internal/fixtures/remove_record_error_missing_id.json
@@ -0,0 +1,7 @@
+{
+ "jsonrpc": "2.0",
+ "Error": {
+ "code": 400,
+ "message": "missing ID"
+ }
+}
diff --git a/providers/dns/otc/internal/client_test.go b/providers/dns/otc/internal/client_test.go
new file mode 100644
index 000000000..ea3835a56
--- /dev/null
+++ b/providers/dns/otc/internal/client_test.go
@@ -0,0 +1,109 @@
+package internal
+
+import (
+ "context"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "testing"
+
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret", "example.com", "test")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
+}
+
+func TestClient_GetZoneID(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /zones",
+ servermock.ResponseFromFixture("zones_GET.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.")).
+ Build(t)
+
+ zoneID, err := client.GetZoneID(context.Background(), "example.com.")
+ require.NoError(t, err)
+
+ assert.Equal(t, "123123", zoneID)
+}
+
+func TestClient_GetZoneID_error(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /zones",
+ servermock.ResponseFromFixture("zones_GET_empty.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.")).
+ Build(t)
+
+ _, err := client.GetZoneID(context.Background(), "example.com.")
+ require.EqualError(t, err, "zone example.com. not found")
+}
+
+func TestClient_GetRecordSetID(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /zones/123123/recordsets",
+ servermock.ResponseFromFixture("zones-recordsets_GET.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.").
+ With("type", "TXT"),
+ ).
+ Build(t)
+
+ recordSetID, err := client.GetRecordSetID(context.Background(), "123123", "example.com.")
+ require.NoError(t, err)
+
+ assert.Equal(t, "321321", recordSetID)
+}
+
+func TestClient_GetRecordSetID_error(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /zones/123123/recordsets",
+ servermock.ResponseFromFixture("zones-recordsets_GET_empty.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.").
+ With("type", "TXT"),
+ ).
+ Build(t)
+
+ _, err := client.GetRecordSetID(context.Background(), "123123", "example.com.")
+ require.EqualError(t, err, "record not found")
+}
+
+func TestClient_CreateRecordSet(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /zones/123123/recordsets",
+ servermock.ResponseFromFixture("zones-recordsets_POST.json")).
+ Build(t)
+
+ rs := RecordSets{
+ Name: "_acme-challenge.example.com.",
+ Description: "Added TXT record for ACME dns-01 challenge using lego client",
+ Type: "TXT",
+ TTL: 300,
+ Records: []string{strconv.Quote("w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI")},
+ }
+ err := client.CreateRecordSet(context.Background(), "123123", rs)
+ require.NoError(t, err)
+}
+
+func TestClient_DeleteRecordSet(t *testing.T) {
+ client := mockBuilder().
+ Route("DELETE /zones/123123/recordsets/321321",
+ servermock.ResponseFromFixture("zones-recordsets_DELETE.json")).
+ Build(t)
+
+ err := client.DeleteRecordSet(context.Background(), "123123", "321321")
+ require.NoError(t, err)
+}
diff --git a/providers/dns/otc/internal/identity_test.go b/providers/dns/otc/internal/identity_test.go
index c8bda7027..4dce72afc 100644
--- a/providers/dns/otc/internal/identity_test.go
+++ b/providers/dns/otc/internal/identity_test.go
@@ -1,24 +1,36 @@
package internal
import (
+ "net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClient_Login(t *testing.T) {
- mock := NewDNSServerMock(t)
- mock.HandleAuthSuccessfully()
+ var serverURL *url.URL
- client := NewClient("user", "secret", "example.com", "test")
- client.IdentityEndpoint, _ = url.JoinPath(mock.GetServerURL(), "/v3/auth/token")
+ client := servermock.NewBuilder(
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret", "example.com", "test")
+ client.HTTPClient = server.Client()
+ client.IdentityEndpoint = server.URL + "/v3/auth/token"
+
+ serverURL, _ = url.Parse(server.URL)
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ ).
+ Route("POST /v3/auth/token", IdentityHandlerMock()).
+ Build(t)
err := client.Login(t.Context())
require.NoError(t, err)
- serverURL, _ := url.Parse(mock.GetServerURL())
assert.Equal(t, serverURL.JoinPath("v2").String(), client.baseURL.String())
assert.Equal(t, fakeOTCToken, client.token)
}
diff --git a/providers/dns/otc/internal/mock.go b/providers/dns/otc/internal/mock.go
index 2ed7f84de..46da61e4c 100644
--- a/providers/dns/otc/internal/mock.go
+++ b/providers/dns/otc/internal/mock.go
@@ -2,62 +2,13 @@ package internal
import (
"fmt"
- "io"
"net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
const fakeOTCToken = "62244bc21da68d03ebac94e6636ff01f"
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("internal", "fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
-}
-
-// DNSServerMock mock.
-type DNSServerMock struct {
- t *testing.T
- server *httptest.Server
- mux *http.ServeMux
-}
-
-// NewDNSServerMock create a new DNSServerMock.
-func NewDNSServerMock(t *testing.T) *DNSServerMock {
- t.Helper()
-
- mux := http.NewServeMux()
-
- return &DNSServerMock{
- t: t,
- server: httptest.NewServer(mux),
- mux: mux,
- }
-}
-
-func (m *DNSServerMock) GetServerURL() string {
- return m.server.URL
-}
-
-// ShutdownServer creates the mock server.
-func (m *DNSServerMock) ShutdownServer() {
- m.server.Close()
-}
-
-// HandleAuthSuccessfully Handle auth successfully.
-func (m *DNSServerMock) HandleAuthSuccessfully() {
- m.mux.HandleFunc("/v3/auth/token", func(w http.ResponseWriter, _ *http.Request) {
+func IdentityHandlerMock() http.HandlerFunc {
+ return func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("X-Subject-Token", fakeOTCToken)
_, _ = fmt.Fprintf(w, `{
@@ -69,7 +20,7 @@ func (m *DNSServerMock) HandleAuthSuccessfully() {
"name": "",
"endpoints": [
{
- "url": "%s",
+ "url": "http://%s",
"region": "eu-de",
"region_id": "eu-de",
"interface": "public",
@@ -78,87 +29,6 @@ func (m *DNSServerMock) HandleAuthSuccessfully() {
]
}
]
- }}`, m.server.URL)
- })
-}
-
-// HandleListZonesSuccessfully Handle list zones successfully.
-func (m *DNSServerMock) HandleListZonesSuccessfully() {
- m.mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(m.t, http.MethodGet, r.Method)
- assert.Equal(m.t, "/v2/zones", r.URL.Path)
- assert.Equal(m.t, "name=example.com.", r.URL.RawQuery)
- assert.Equal(m.t, "application/json", r.Header.Get("Accept"))
-
- writeFixture(w, "zones_GET.json")
- })
-}
-
-// HandleListZonesEmpty Handle list zones empty.
-func (m *DNSServerMock) HandleListZonesEmpty() {
- m.mux.HandleFunc("/v2/zones", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(m.t, http.MethodGet, r.Method)
- assert.Equal(m.t, "/v2/zones", r.URL.Path)
- assert.Equal(m.t, "name=example.com.", r.URL.RawQuery)
- assert.Equal(m.t, "application/json", r.Header.Get("Accept"))
-
- writeFixture(w, "zones_GET_empty.json")
- })
-}
-
-// HandleDeleteRecordsetsSuccessfully Handle delete recordsets successfully.
-func (m *DNSServerMock) HandleDeleteRecordsetsSuccessfully() {
- m.mux.HandleFunc("/v2/zones/123123/recordsets/321321", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(m.t, http.MethodDelete, r.Method)
- assert.Equal(m.t, "/v2/zones/123123/recordsets/321321", r.URL.Path)
- assert.Equal(m.t, "application/json", r.Header.Get("Accept"))
-
- writeFixture(w, "zones-recordsets_DELETE.json")
- })
-}
-
-// HandleListRecordsetsEmpty Handle list recordsets empty.
-func (m *DNSServerMock) HandleListRecordsetsEmpty() {
- m.mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(m.t, "/v2/zones/123123/recordsets", r.URL.Path)
- assert.Equal(m.t, "name=_acme-challenge.example.com.&type=TXT", r.URL.RawQuery)
-
- writeFixture(w, "zones-recordsets_GET_empty.json")
- })
-}
-
-// HandleListRecordsetsSuccessfully Handle list recordsets successfully.
-func (m *DNSServerMock) HandleListRecordsetsSuccessfully() {
- m.mux.HandleFunc("/v2/zones/123123/recordsets", func(w http.ResponseWriter, r *http.Request) {
- assert.Equal(m.t, "application/json", r.Header.Get("Accept"))
-
- if r.Method == http.MethodGet {
- assert.Equal(m.t, "/v2/zones/123123/recordsets", r.URL.Path)
- assert.Equal(m.t, "name=_acme-challenge.example.com.&type=TXT", r.URL.RawQuery)
-
- writeFixture(w, "zones-recordsets_GET.json")
- return
- }
-
- if r.Method == http.MethodPost {
- assert.Equal(m.t, "application/json", r.Header.Get("Content-Type"))
-
- raw, err := io.ReadAll(r.Body)
- require.NoError(m.t, err)
- exceptedString := `{
- "name": "_acme-challenge.example.com.",
- "description": "Added TXT record for ACME dns-01 challenge using lego client",
- "type": "TXT",
- "ttl": 300,
- "records": ["\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\""]
- }`
-
- assert.JSONEq(m.t, exceptedString, string(raw))
-
- writeFixture(w, "zones-recordsets_POST.json")
- return
- }
-
- http.Error(w, fmt.Sprintf("Expected method to be 'GET' or 'POST' but got '%s'", r.Method), http.StatusBadRequest)
- })
+ }}`, req.Context().Value(http.LocalAddrContextKey))
+ }
}
diff --git a/providers/dns/otc/otc_test.go b/providers/dns/otc/otc_test.go
index 54907b69e..1e53f31cc 100644
--- a/providers/dns/otc/otc_test.go
+++ b/providers/dns/otc/otc_test.go
@@ -2,129 +2,296 @@ package otc
import (
"fmt"
- "os"
+ "net/http/httptest"
+ "path"
"testing"
+ "time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/go-acme/lego/v4/providers/dns/otc/internal"
- "github.com/stretchr/testify/suite"
+ "github.com/stretchr/testify/require"
)
-type OTCSuite struct {
- suite.Suite
+const envDomain = envNamespace + "DOMAIN"
- mock *internal.DNSServerMock
- envTest *tester.EnvTest
+var envTest = tester.NewEnvTest(
+ EnvDomainName,
+ EnvUserName,
+ EnvPassword,
+ EnvProjectName,
+ EnvIdentityEndpoint).
+ WithDomain(envDomain)
+
+func TestNewDNSProvider(t *testing.T) {
+ testCases := []struct {
+ desc string
+ envVars map[string]string
+ expected string
+ }{
+ {
+ desc: "success",
+ envVars: map[string]string{
+ EnvDomainName: "example.com",
+ EnvUserName: "user",
+ EnvPassword: "secret",
+ EnvProjectName: "test",
+ },
+ },
+ {
+ desc: "missing credentials",
+ envVars: map[string]string{
+ EnvDomainName: "",
+ EnvUserName: "",
+ EnvPassword: "",
+ EnvProjectName: "",
+ },
+ expected: "otc: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME",
+ },
+ {
+ desc: "missing domain name",
+ envVars: map[string]string{
+ EnvDomainName: "",
+ EnvUserName: "user",
+ EnvPassword: "secret",
+ EnvProjectName: "test",
+ },
+ expected: "otc: some credentials information are missing: OTC_DOMAIN_NAME",
+ },
+ {
+ desc: "missing username",
+ envVars: map[string]string{
+ EnvDomainName: "example.com",
+ EnvUserName: "",
+ EnvPassword: "secret",
+ EnvProjectName: "test",
+ },
+ expected: "otc: some credentials information are missing: OTC_USER_NAME",
+ },
+ {
+ desc: "missing password",
+ envVars: map[string]string{
+ EnvDomainName: "example.com",
+ EnvUserName: "user",
+ EnvPassword: "",
+ EnvProjectName: "test",
+ },
+ expected: "otc: some credentials information are missing: OTC_PASSWORD",
+ },
+ {
+ desc: "missing project name",
+ envVars: map[string]string{
+ EnvDomainName: "example.com",
+ EnvUserName: "user",
+ EnvPassword: "secret",
+ EnvProjectName: "",
+ },
+ expected: "otc: some credentials information are missing: OTC_PROJECT_NAME",
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ defer envTest.RestoreEnv()
+ envTest.ClearEnv()
+
+ envTest.Apply(test.envVars)
+
+ p, err := NewDNSProvider()
+
+ if test.expected == "" {
+ require.NoError(t, err)
+ require.NotNil(t, p)
+ require.NotNil(t, p.config)
+ } else {
+ require.EqualError(t, err, test.expected)
+ }
+ })
+ }
}
-func (s *OTCSuite) SetupTest() {
- s.mock = internal.NewDNSServerMock(s.T())
- s.mock.HandleAuthSuccessfully()
- s.envTest = tester.NewEnvTest(
- EnvDomainName,
- EnvUserName,
- EnvPassword,
- EnvProjectName,
- EnvIdentityEndpoint,
- )
+func TestNewDNSProviderConfig(t *testing.T) {
+ testCases := []struct {
+ desc string
+ domainName string
+ projectName string
+ username string
+ password string
+ expected string
+ }{
+ {
+ desc: "success",
+ domainName: "example.com",
+ projectName: "test",
+ username: "user",
+ password: "secret",
+ },
+ {
+ desc: "missing credentials",
+ expected: "otc: credentials missing",
+ },
+ {
+ desc: "missing domain name",
+ domainName: "",
+ projectName: "test",
+ username: "user",
+ password: "secret",
+ expected: "otc: credentials missing",
+ },
+ {
+ desc: "missing project name",
+ domainName: "example.com",
+ projectName: "",
+ username: "user",
+ password: "secret",
+ expected: "otc: credentials missing",
+ },
+ {
+ desc: "missing username",
+ domainName: "example.com",
+ projectName: "test",
+ username: "",
+ password: "secret",
+ expected: "otc: credentials missing",
+ },
+ {
+ desc: "missing password ",
+ domainName: "example.com",
+ projectName: "test",
+ username: "user",
+ password: "",
+ expected: "otc: credentials missing",
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ config := NewDefaultConfig()
+ config.DomainName = test.domainName
+ config.ProjectName = test.projectName
+ config.UserName = test.username
+ config.Password = test.password
+
+ p, err := NewDNSProviderConfig(config)
+
+ if test.expected == "" {
+ require.NoError(t, err)
+ require.NotNil(t, p)
+ require.NotNil(t, p.config)
+ } else {
+ require.EqualError(t, err, test.expected)
+ }
+ })
+ }
}
-func (s *OTCSuite) TearDownTest() {
- s.envTest.RestoreEnv()
- s.mock.ShutdownServer()
-}
-
-func TestTestSuite(t *testing.T) {
- suite.Run(t, new(OTCSuite))
-}
-
-func (s *OTCSuite) createDNSProvider() (*DNSProvider, error) {
- config := NewDefaultConfig()
- config.UserName = "UserName"
- config.Password = "Password"
- config.DomainName = "DomainName"
- config.ProjectName = "ProjectName"
- config.IdentityEndpoint = fmt.Sprintf("%s/v3/auth/token", s.mock.GetServerURL())
-
- return NewDNSProviderConfig(config)
-}
-
-func (s *OTCSuite) TestLoginEnv() {
- s.envTest.ClearEnv()
-
- s.envTest.Apply(map[string]string{
- EnvDomainName: "unittest1",
- EnvUserName: "unittest2",
- EnvPassword: "unittest3",
- EnvProjectName: "unittest4",
- EnvIdentityEndpoint: "unittest5",
- })
+func TestLivePresent(t *testing.T) {
+ if !envTest.IsLiveTest() {
+ t.Skip("skipping live test")
+ }
+ envTest.RestoreEnv()
provider, err := NewDNSProvider()
- s.Require().NoError(err)
+ require.NoError(t, err)
- s.Equal("unittest1", provider.config.DomainName)
- s.Equal("unittest2", provider.config.UserName)
- s.Equal("unittest3", provider.config.Password)
- s.Equal("unittest4", provider.config.ProjectName)
- s.Equal("unittest5", provider.config.IdentityEndpoint)
-
- os.Setenv(EnvIdentityEndpoint, "")
-
- provider, err = NewDNSProvider()
- s.Require().NoError(err)
-
- s.Equal("https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens", provider.config.IdentityEndpoint)
+ err = provider.Present(envTest.GetDomain(), "", "123d==")
+ require.NoError(t, err)
}
-func (s *OTCSuite) TestLoginEnvEmpty() {
- s.envTest.ClearEnv()
+func TestLiveCleanUp(t *testing.T) {
+ if !envTest.IsLiveTest() {
+ t.Skip("skipping live test")
+ }
- _, err := NewDNSProvider()
- s.EqualError(err, "otc: some credentials information are missing: OTC_DOMAIN_NAME,OTC_USER_NAME,OTC_PASSWORD,OTC_PROJECT_NAME")
+ envTest.RestoreEnv()
+ provider, err := NewDNSProvider()
+ require.NoError(t, err)
+
+ time.Sleep(1 * time.Second)
+
+ err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
+ require.NoError(t, err)
}
-func (s *OTCSuite) TestDNSProvider_Present() {
- s.mock.HandleListZonesSuccessfully()
- s.mock.HandleListRecordsetsSuccessfully()
+func TestDNSProvider_Present(t *testing.T) {
+ provider := mockBuilder().
+ Route("GET /v2/zones",
+ responseFromFixture("zones_GET.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.")).
+ Route("/", servermock.DumpRequest()).
+ Build(t)
- provider, err := s.createDNSProvider()
- s.Require().NoError(err)
-
- err = provider.Present("example.com", "", "foobar")
- s.Require().NoError(err)
+ err := provider.Present("example.com", "", "123d==")
+ require.NoError(t, err)
}
-func (s *OTCSuite) TestDNSProvider_Present_EmptyZone() {
- s.mock.HandleListZonesEmpty()
- s.mock.HandleListRecordsetsSuccessfully()
+func TestDNSProvider_Present_emptyZone(t *testing.T) {
+ provider := mockBuilder().
+ Route("GET /v2/zones",
+ responseFromFixture("zones_GET_empty.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.")).
+ Route("/", servermock.DumpRequest()).
+ Build(t)
- provider, err := s.createDNSProvider()
- s.Require().NoError(err)
-
- err = provider.Present("example.com", "", "foobar")
- s.Error(err)
+ err := provider.Present("example.com", "", "123d==")
+ require.EqualError(t, err, "otc: unable to get zone: zone example.com. not found")
}
-func (s *OTCSuite) TestDNSProvider_CleanUp() {
- s.mock.HandleListZonesSuccessfully()
- s.mock.HandleListRecordsetsSuccessfully()
- s.mock.HandleDeleteRecordsetsSuccessfully()
+func TestDNSProvider_Cleanup(t *testing.T) {
+ provider := mockBuilder().
+ Route("GET /v2/zones",
+ responseFromFixture("zones_GET.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.")).
+ Route("GET /v2/zones/123123/recordsets",
+ responseFromFixture("zones-recordsets_GET.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge.example.com.").
+ With("type", "TXT")).
+ Route("DELETE /v2/zones/123123/recordsets/321321",
+ responseFromFixture("zones-recordsets_DELETE.json")).
+ Build(t)
- provider, err := s.createDNSProvider()
- s.Require().NoError(err)
-
- err = provider.CleanUp("example.com", "", "foobar")
- s.Require().NoError(err)
+ err := provider.CleanUp("example.com", "", "123d==")
+ require.NoError(t, err)
}
-func (s *OTCSuite) TestDNSProvider_CleanUp_EmptyRecordset() {
- s.mock.HandleListZonesSuccessfully()
- s.mock.HandleListRecordsetsEmpty()
+func TestDNSProvider_Cleanup_emptyRecordset(t *testing.T) {
+ provider := mockBuilder().
+ Route("GET /v2/zones",
+ responseFromFixture("zones_GET.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com.")).
+ Route("GET /v2/zones/123123/recordsets",
+ responseFromFixture("zones-recordsets_GET_empty.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge.example.com.").
+ With("type", "TXT")).
+ Build(t)
- provider, err := s.createDNSProvider()
- s.Require().NoError(err)
-
- err = provider.CleanUp("example.com", "", "foobar")
- s.Require().Error(err)
+ err := provider.CleanUp("example.com", "", "123d==")
+ require.EqualError(t, err, "otc: unable to get record _acme-challenge.example.com. for zone example.com: record not found")
+}
+
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.UserName = "user"
+ config.Password = "secret"
+ config.DomainName = "example.com"
+ config.ProjectName = "test"
+ config.IdentityEndpoint = fmt.Sprintf("%s/v3/auth/token", server.URL)
+
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ ).
+ Route("POST /v3/auth/token", internal.IdentityHandlerMock())
+}
+
+func responseFromFixture(filename string) *servermock.ResponseFromFileHandler {
+ return servermock.ResponseFromFile(path.Join("internal", "fixtures", filename))
}
diff --git a/providers/dns/pdns/internal/client.go b/providers/dns/pdns/internal/client.go
index bc525c578..f6b55d5de 100644
--- a/providers/dns/pdns/internal/client.go
+++ b/providers/dns/pdns/internal/client.go
@@ -18,6 +18,9 @@ import (
"github.com/miekg/dns"
)
+// APIKeyHeader API key header.
+const APIKeyHeader = "X-Api-Key"
+
// Client the PowerDNS API client.
type Client struct {
serverName string
@@ -163,7 +166,7 @@ func (c *Client) joinPath(elem ...string) *url.URL {
}
func (c *Client) do(req *http.Request) (json.RawMessage, error) {
- req.Header.Set("X-API-Key", c.apiKey)
+ req.Header.Set(APIKeyHeader, c.apiKey)
resp, err := c.HTTPClient.Do(req)
if err != nil {
diff --git a/providers/dns/pdns/internal/client_test.go b/providers/dns/pdns/internal/client_test.go
index d3919ace3..6d1c48852 100644
--- a/providers/dns/pdns/internal/client_test.go
+++ b/providers/dns/pdns/internal/client_test.go
@@ -1,65 +1,27 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ serverURL, _ := url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client := NewClient(serverURL, "server", 0, "secret")
+ client.HTTPClient = server.Client()
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- apiKey := req.Header.Get("X-API-Key")
- if apiKey != "secret" {
- http.Error(rw, fmt.Sprintf("invalid credentials: %s", apiKey), http.StatusBadRequest)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- serverURL, _ := url.Parse(server.URL)
-
- client := NewClient(serverURL, "server", 0, "secret")
- client.HTTPClient = server.Client()
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().With(APIKeyHeader, "secret"))
}
func TestClient_joinPath(t *testing.T) {
@@ -159,7 +121,11 @@ func TestClient_joinPath(t *testing.T) {
}
func TestClient_GetHostedZone(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/api/v1/servers/server/zones/example.org.", http.StatusOK, "zone.json")
+ client := mockBuilder().
+ Route("GET /api/v1/servers/server/zones/example.org.",
+ servermock.ResponseFromFixture("zone.json")).
+ Build(t)
+
client.apiVersion = 1
zone, err := client.GetHostedZone(t.Context(), "example.org.")
@@ -202,7 +168,12 @@ func TestClient_GetHostedZone(t *testing.T) {
}
func TestClient_GetHostedZone_error(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/api/v1/servers/server/zones/example.org.", http.StatusUnprocessableEntity, "error.json")
+ client := mockBuilder().
+ Route("GET /api/v1/servers/server/zones/example.org.",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnprocessableEntity)).
+ Build(t)
+
client.apiVersion = 1
_, err := client.GetHostedZone(t.Context(), "example.org.")
@@ -210,7 +181,11 @@ func TestClient_GetHostedZone_error(t *testing.T) {
}
func TestClient_GetHostedZone_v0(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/servers/server/zones/example.org.", http.StatusOK, "zone.json")
+ client := mockBuilder().
+ Route("GET /servers/server/zones/example.org.",
+ servermock.ResponseFromFixture("zone.json")).
+ Build(t)
+
client.apiVersion = 0
zone, err := client.GetHostedZone(t.Context(), "example.org.")
@@ -253,7 +228,12 @@ func TestClient_GetHostedZone_v0(t *testing.T) {
}
func TestClient_UpdateRecords(t *testing.T) {
- client := setupTest(t, http.MethodPatch, "/api/v1/servers/localhost/zones/example.org.", http.StatusOK, "zone.json")
+ client := mockBuilder().
+ Route("PATCH /api/v1/servers/localhost/zones/example.org.",
+ servermock.ResponseFromFixture("zone.json"),
+ servermock.CheckRequestJSONBodyFromFile("zone-request.json")).
+ Build(t)
+
client.apiVersion = 1
client.serverName = "localhost"
@@ -283,7 +263,12 @@ func TestClient_UpdateRecords(t *testing.T) {
}
func TestClient_UpdateRecords_NonRootApi(t *testing.T) {
- client := setupTest(t, http.MethodPatch, "/some/path/api/v1/servers/localhost/zones/example.org.", http.StatusOK, "zone.json")
+ client := mockBuilder().
+ Route("PATCH /some/path/api/v1/servers/localhost/zones/example.org.",
+ servermock.ResponseFromFixture("zone.json"),
+ servermock.CheckRequestJSONBodyFromFile("zone-request.json")).
+ Build(t)
+
client.Host = client.Host.JoinPath("some", "path")
client.apiVersion = 1
client.serverName = "localhost"
@@ -314,7 +299,12 @@ func TestClient_UpdateRecords_NonRootApi(t *testing.T) {
}
func TestClient_UpdateRecords_v0(t *testing.T) {
- client := setupTest(t, http.MethodPatch, "/servers/localhost/zones/example.org.", http.StatusOK, "zone.json")
+ client := mockBuilder().
+ Route("PATCH /servers/localhost/zones/example.org.",
+ servermock.ResponseFromFixture("zone.json"),
+ servermock.CheckRequestJSONBodyFromFile("zone-request.json")).
+ Build(t)
+
client.apiVersion = 0
client.serverName = "localhost"
@@ -344,7 +334,10 @@ func TestClient_UpdateRecords_v0(t *testing.T) {
}
func TestClient_Notify(t *testing.T) {
- client := setupTest(t, http.MethodPut, "/api/v1/servers/localhost/zones/example.org./notify", http.StatusOK, "")
+ client := mockBuilder().
+ Route("PUT /api/v1/servers/localhost/zones/example.org./notify", nil).
+ Build(t)
+
client.apiVersion = 1
client.serverName = "localhost"
@@ -360,7 +353,10 @@ func TestClient_Notify(t *testing.T) {
}
func TestClient_Notify_NonRootApi(t *testing.T) {
- client := setupTest(t, http.MethodPut, "/some/path/api/v1/servers/localhost/zones/example.org./notify", http.StatusOK, "")
+ client := mockBuilder().
+ Route("PUT /some/path/api/v1/servers/localhost/zones/example.org./notify", nil).
+ Build(t)
+
client.Host = client.Host.JoinPath("some", "path")
client.apiVersion = 1
client.serverName = "localhost"
@@ -377,7 +373,10 @@ func TestClient_Notify_NonRootApi(t *testing.T) {
}
func TestClient_Notify_v0(t *testing.T) {
- client := setupTest(t, http.MethodPut, "/api/v1/servers/localhost/zones/example.org./notify", http.StatusOK, "")
+ client := mockBuilder().
+ Route("PUT /some/path/api/v1/servers/localhost/zones/example.org./notify", nil).
+ Build(t)
+
client.apiVersion = 0
zone := &HostedZone{
@@ -392,7 +391,10 @@ func TestClient_Notify_v0(t *testing.T) {
}
func TestClient_getAPIVersion(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/api", http.StatusOK, "versions.json")
+ client := mockBuilder().
+ Route("GET /api",
+ servermock.ResponseFromFixture("versions.json")).
+ Build(t)
version, err := client.getAPIVersion(t.Context())
require.NoError(t, err)
diff --git a/providers/dns/pdns/internal/fixtures/zone-request.json b/providers/dns/pdns/internal/fixtures/zone-request.json
new file mode 100644
index 000000000..5e4a6d2b9
--- /dev/null
+++ b/providers/dns/pdns/internal/fixtures/zone-request.json
@@ -0,0 +1,19 @@
+{
+ "rrsets": [
+ {
+ "name": "example.org.",
+ "type": "NS",
+ "kind": "",
+ "changetype": "REPLACE",
+ "records": [
+ {
+ "content": "192.0.2.5",
+ "disabled": false,
+ "name": "ns1.example.org.",
+ "type": "A",
+ "ttl": 86400
+ }
+ ]
+ }
+ ]
+}
diff --git a/providers/dns/plesk/internal/client_test.go b/providers/dns/plesk/internal/client_test.go
index b61bce4c2..14cadd0e0 100644
--- a/providers/dns/plesk/internal/client_test.go
+++ b/providers/dns/plesk/internal/client_test.go
@@ -1,69 +1,35 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ serverURL, _ := url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client := NewClient(serverURL, "user", "secret")
+ client.HTTPClient = server.Client()
- serverURL, err := url.Parse(server.URL)
- require.NoError(t, err)
-
- client := NewClient(serverURL, "user", "secret")
- client.HTTPClient = server.Client()
-
- mux.HandleFunc("/enterprise/control/agent.php", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- login := req.Header.Get("Http_auth_login")
- if login != "user" {
- http.Error(rw, fmt.Sprintf("invalid login: %s", login), http.StatusUnauthorized)
- return
- }
-
- password := req.Header.Get("Http_auth_passwd")
- if password != "secret" {
- http.Error(rw, fmt.Sprintf("invalid password: %s", password), http.StatusUnauthorized)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithContentType("text/xml").
+ With("Http_auth_login", "user").
+ With("Http_auth_passwd", "secret"),
+ )
}
func TestClient_GetSite(t *testing.T) {
- client := setupTest(t, "get-site.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("get-site.xml")).
+ Build(t)
siteID, err := client.GetSite(t.Context(), "example.com")
require.NoError(t, err)
@@ -72,7 +38,9 @@ func TestClient_GetSite(t *testing.T) {
}
func TestClient_GetSite_error(t *testing.T) {
- client := setupTest(t, "get-site-error.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("get-site-error.xml")).
+ Build(t)
siteID, err := client.GetSite(t.Context(), "example.com")
require.Error(t, err)
@@ -81,7 +49,9 @@ func TestClient_GetSite_error(t *testing.T) {
}
func TestClient_GetSite_system_error(t *testing.T) {
- client := setupTest(t, "global-error.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("global-error.xml")).
+ Build(t)
siteID, err := client.GetSite(t.Context(), "example.com")
require.Error(t, err)
@@ -90,7 +60,9 @@ func TestClient_GetSite_system_error(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "add-record.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("add-record.xml")).
+ Build(t)
recordID, err := client.AddRecord(t.Context(), 123, "_acme-challenge.example.com", "txtTXTtxt")
require.NoError(t, err)
@@ -99,7 +71,9 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "add-record-error.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("add-record-error.xml")).
+ Build(t)
recordID, err := client.AddRecord(t.Context(), 123, "_acme-challenge.example.com", "txtTXTtxt")
require.ErrorAs(t, err, new(RecResult))
@@ -108,7 +82,9 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_AddRecord_system_error(t *testing.T) {
- client := setupTest(t, "global-error.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("global-error.xml")).
+ Build(t)
recordID, err := client.AddRecord(t.Context(), 123, "_acme-challenge.example.com", "txtTXTtxt")
require.ErrorAs(t, err, new(*System))
@@ -117,7 +93,9 @@ func TestClient_AddRecord_system_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "delete-record.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("delete-record.xml")).
+ Build(t)
recordID, err := client.DeleteRecord(t.Context(), 4537)
require.NoError(t, err)
@@ -126,7 +104,9 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "delete-record-error.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("delete-record-error.xml")).
+ Build(t)
recordID, err := client.DeleteRecord(t.Context(), 4537)
require.ErrorAs(t, err, new(RecResult))
@@ -135,7 +115,9 @@ func TestClient_DeleteRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord_system_error(t *testing.T) {
- client := setupTest(t, "global-error.xml")
+ client := mockBuilder().
+ Route("POST /enterprise/control/agent.php", servermock.ResponseFromFixture("global-error.xml")).
+ Build(t)
recordID, err := client.DeleteRecord(t.Context(), 4537)
require.ErrorAs(t, err, new(*System))
diff --git a/providers/dns/rackspace/fixtures/delete.json b/providers/dns/rackspace/fixtures/delete.json
new file mode 100644
index 000000000..7e2f2ac53
--- /dev/null
+++ b/providers/dns/rackspace/fixtures/delete.json
@@ -0,0 +1,7 @@
+{
+ "status": "RUNNING",
+ "verb": "DELETE",
+ "jobId": "00000000-0000-0000-0000-0000000000",
+ "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000",
+ "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321"
+}
diff --git a/providers/dns/rackspace/fixtures/identity.json b/providers/dns/rackspace/fixtures/identity.json
new file mode 100644
index 000000000..5a459d13c
--- /dev/null
+++ b/providers/dns/rackspace/fixtures/identity.json
@@ -0,0 +1,31 @@
+{
+ "access": {
+ "token": {
+ "id": "testToken",
+ "expires": "1970-01-01T00:00:00.000Z",
+ "tenant": {
+ "id": "123456",
+ "name": "123456"
+ },
+ "RAX-AUTH:authenticatedBy": [
+ "APIKEY"
+ ]
+ },
+ "serviceCatalog": [
+ {
+ "type": "rax:dns",
+ "endpoints": [
+ {
+ "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456",
+ "tenantId": "123456"
+ }
+ ],
+ "name": "cloudDNS"
+ }
+ ],
+ "user": {
+ "id": "fakeUseID",
+ "name": "testUser"
+ }
+ }
+}
diff --git a/providers/dns/rackspace/fixtures/record.json b/providers/dns/rackspace/fixtures/record.json
new file mode 100644
index 000000000..4d76aa0c8
--- /dev/null
+++ b/providers/dns/rackspace/fixtures/record.json
@@ -0,0 +1,8 @@
+{
+ "request": "{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}",
+ "status": "RUNNING",
+ "verb": "POST",
+ "jobId": "00000000-0000-0000-0000-0000000000",
+ "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000",
+ "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records"
+}
diff --git a/providers/dns/rackspace/fixtures/record_details.json b/providers/dns/rackspace/fixtures/record_details.json
new file mode 100644
index 000000000..e53cf1330
--- /dev/null
+++ b/providers/dns/rackspace/fixtures/record_details.json
@@ -0,0 +1,13 @@
+{
+ "records": [
+ {
+ "name": "_acme-challenge.example.com",
+ "id": "TXT-654321",
+ "type": "TXT",
+ "data": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM",
+ "ttl": 300,
+ "updated": "1970-01-01T00:00:00.000+0000",
+ "created": "1970-01-01T00:00:00.000+0000"
+ }
+ ]
+}
diff --git a/providers/dns/rackspace/fixtures/zone_details.json b/providers/dns/rackspace/fixtures/zone_details.json
new file mode 100644
index 000000000..f68f23aa0
--- /dev/null
+++ b/providers/dns/rackspace/fixtures/zone_details.json
@@ -0,0 +1,12 @@
+{
+ "domains": [
+ {
+ "name": "example.com",
+ "id": "112233",
+ "emailAddress": "hostmaster@example.com",
+ "updated": "1970-01-01T00:00:00.000+0000",
+ "created": "1970-01-01T00:00:00.000+0000"
+ }
+ ],
+ "totalEntries": 1
+}
diff --git a/providers/dns/rackspace/internal/client.go b/providers/dns/rackspace/internal/client.go
index de25f8d0e..076409ebd 100644
--- a/providers/dns/rackspace/internal/client.go
+++ b/providers/dns/rackspace/internal/client.go
@@ -14,6 +14,8 @@ import (
"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)
+const AuthToken = "X-Auth-Token"
+
type Client struct {
token string
@@ -34,7 +36,7 @@ func NewClient(endpoint, token string) (*Client, error) {
}, nil
}
-// AddRecord Adds one record to a specified domain.
+// AddRecord Adds one record to a specified domain.
// https://docs.rackspace.com/docs/cloud-dns/v1/api-reference/records#add-records
func (c *Client) AddRecord(ctx context.Context, zoneID string, record Record) error {
endpoint := c.baseURL.JoinPath("domains", zoneID, "records")
@@ -161,7 +163,7 @@ func (c *Client) searchRecords(ctx context.Context, zoneID, recordName, recordTy
}
func (c *Client) do(req *http.Request, result any) error {
- req.Header.Set("X-Auth-Token", c.token)
+ req.Header.Set(AuthToken, c.token)
resp, err := c.HTTPClient.Do(req)
if err != nil {
diff --git a/providers/dns/rackspace/internal/client_test.go b/providers/dns/rackspace/internal/client_test.go
index ce25d107c..c14c4d360 100644
--- a/providers/dns/rackspace/internal/client_test.go
+++ b/providers/dns/rackspace/internal/client_test.go
@@ -1,78 +1,62 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient(server.URL, "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
- client, err := NewClient(server.URL, "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
-
- mux.HandleFunc(pattern, handler)
-
- return client
-}
-
-func writeFixtureHandler(method, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- if req.Header.Get("X-Auth-Token") != "secret" {
- http.Error(rw, fmt.Sprintf("invalid token: %q", req.Header.Get("X-Auth-Token")), http.StatusUnauthorized)
- return
- }
-
- if filename == "" {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
- }
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With(AuthToken, "secret"))
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/domains/1234/records", writeFixtureHandler(http.MethodPost, "add-records.json"))
+ client := mockBuilder().
+ Route("POST /domains/1234/records",
+ servermock.ResponseFromFixture("add-records.json"),
+ servermock.CheckRequestJSONBody(`{"records":[{"name":"exmaple.com","type":"TXT","data":"value1","ttl":120,"id":"abc"}]}`)).
+ Build(t)
- err := client.AddRecord(t.Context(), "1234", Record{})
+ record := Record{
+ Name: "exmaple.com",
+ Type: "TXT",
+ Data: "value1",
+ TTL: 120,
+ ID: "abc",
+ }
+
+ err := client.AddRecord(t.Context(), "1234", record)
require.NoError(t, err)
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/domains/1234/records", writeFixtureHandler(http.MethodDelete, ""))
+ client := mockBuilder().
+ Route("DELETE /domains/1234/records", nil).
+ Build(t)
err := client.DeleteRecord(t.Context(), "1234", "2725233")
require.NoError(t, err)
}
func TestClient_searchRecords(t *testing.T) {
- client := setupTest(t, "/domains/1234/records", writeFixtureHandler(http.MethodGet, "search-records.json"))
+ client := mockBuilder().
+ Route("GET /domains/1234/records", servermock.ResponseFromFixture("search-records.json")).
+ Build(t)
records, err := client.searchRecords(t.Context(), "1234", "2725233", "A")
require.NoError(t, err)
@@ -93,7 +77,9 @@ func TestClient_searchRecords(t *testing.T) {
}
func TestClient_listDomainsByName(t *testing.T) {
- client := setupTest(t, "/domains", writeFixtureHandler(http.MethodGet, "list-domains-by-name.json"))
+ client := mockBuilder().
+ Route("GET /domains", servermock.ResponseFromFixture("list-domains-by-name.json")).
+ Build(t)
domains, err := client.listDomainsByName(t.Context(), "1234")
require.NoError(t, err)
diff --git a/providers/dns/rackspace/internal/identity_test.go b/providers/dns/rackspace/internal/identity_test.go
index b976fdd2f..44a8d75fc 100644
--- a/providers/dns/rackspace/internal/identity_test.go
+++ b/providers/dns/rackspace/internal/identity_test.go
@@ -1,48 +1,22 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func writeIdentityFixtureHandler(method, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- if filename == "" {
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
- }
+func setupIdentifier(server *httptest.Server) (*Identifier, error) {
+ return NewIdentifier(server.Client(), server.URL), nil
}
func TestIdentifier_Login(t *testing.T) {
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- identifier := NewIdentifier(server.Client(), server.URL)
-
- mux.HandleFunc("/", writeIdentityFixtureHandler(http.MethodPost, "tokens.json"))
+ identifier := servermock.NewBuilder[*Identifier](setupIdentifier, servermock.CheckHeader().WithJSONHeaders()).
+ Route("POST /", servermock.ResponseFromFixture("tokens.json")).
+ Build(t)
identity, err := identifier.Login(t.Context(), "user", "secret")
require.NoError(t, err)
diff --git a/providers/dns/rackspace/rackspace_mock_test.go b/providers/dns/rackspace/rackspace_mock_test.go
deleted file mode 100644
index 790d52498..000000000
--- a/providers/dns/rackspace/rackspace_mock_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package rackspace
-
-const recordDeleteMock = `
-{
- "status": "RUNNING",
- "verb": "DELETE",
- "jobId": "00000000-0000-0000-0000-0000000000",
- "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000",
- "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/recordsid=TXT-654321"
-}
-`
-
-const recordDetailsMock = `
-{
- "records": [
- {
- "name": "_acme-challenge.example.com",
- "id": "TXT-654321",
- "type": "TXT",
- "data": "pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM",
- "ttl": 300,
- "updated": "1970-01-01T00:00:00.000+0000",
- "created": "1970-01-01T00:00:00.000+0000"
- }
- ]
-}
-`
-
-const zoneDetailsMock = `
-{
- "domains": [
- {
- "name": "example.com",
- "id": "112233",
- "emailAddress": "hostmaster@example.com",
- "updated": "1970-01-01T00:00:00.000+0000",
- "created": "1970-01-01T00:00:00.000+0000"
- }
- ],
- "totalEntries": 1
-}
-`
-
-const identityResponseMock = `
-{
- "access": {
- "token": {
- "id": "testToken",
- "expires": "1970-01-01T00:00:00.000Z",
- "tenant": {
- "id": "123456",
- "name": "123456"
- },
- "RAX-AUTH:authenticatedBy": [
- "APIKEY"
- ]
- },
- "serviceCatalog": [
- {
- "type": "rax:dns",
- "endpoints": [
- {
- "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456",
- "tenantId": "123456"
- }
- ],
- "name": "cloudDNS"
- }
- ],
- "user": {
- "id": "fakeUseID",
- "name": "testUser"
- }
- }
-}
-`
-
-const recordResponseMock = `
-{
- "request": "{\"records\":[{\"name\":\"_acme-challenge.example.com\",\"type\":\"TXT\",\"data\":\"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM\",\"ttl\":300}]}",
- "status": "RUNNING",
- "verb": "POST",
- "jobId": "00000000-0000-0000-0000-0000000000",
- "callbackUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/status/00000000-0000-0000-0000-0000000000",
- "requestUrl": "https://dns.api.rackspacecloud.com/v1.0/123456/domains/112233/records"
-}
-`
diff --git a/providers/dns/rackspace/rackspace_test.go b/providers/dns/rackspace/rackspace_test.go
index cbc57b472..cefb46134 100644
--- a/providers/dns/rackspace/rackspace_test.go
+++ b/providers/dns/rackspace/rackspace_test.go
@@ -1,9 +1,7 @@
package rackspace
import (
- "bytes"
"fmt"
- "io"
"net/http"
"net/http/httptest"
"strings"
@@ -11,6 +9,7 @@ import (
"time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -23,11 +22,7 @@ var envTest = tester.NewEnvTest(
WithDomain(envDomain)
func TestNewDNSProviderConfig(t *testing.T) {
- config := setupTest(t)
-
- provider, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
- assert.NotNil(t, provider.config)
+ provider := mockBuilder().Build(t)
assert.Equal(t, "testToken", provider.token, "The token should match")
}
@@ -38,25 +33,40 @@ func TestNewDNSProviderConfig_MissingCredErr(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- config := setupTest(t)
+ provider := mockBuilder().
+ Route("GET /123456/domains",
+ servermock.ResponseFromFixture("zone_details.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com")).
+ Route("POST /123456/domains/112233/records",
+ servermock.ResponseFromFixture("record.json").
+ WithStatusCode(http.StatusAccepted),
+ servermock.CheckRequestJSONBody(`{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}`)).
+ Build(t)
- provider, err := NewDNSProviderConfig(config)
-
- if assert.NoError(t, err) {
- err = provider.Present("example.com", "token", "keyAuth")
- require.NoError(t, err)
- }
+ err := provider.Present("example.com", "token", "keyAuth")
+ require.NoError(t, err)
}
func TestDNSProvider_CleanUp(t *testing.T) {
- config := setupTest(t)
+ provider := mockBuilder().
+ Route("GET /123456/domains",
+ servermock.ResponseFromFixture("zone_details.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "example.com")).
+ Route("GET /123456/domains/112233/records",
+ servermock.ResponseFromFixture("record_details.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("type", "TXT").
+ With("name", "_acme-challenge.example.com")).
+ Route("DELETE /123456/domains/112233/records",
+ servermock.ResponseFromFixture("delete.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("id", "TXT-654321")).
+ Build(t)
- provider, err := NewDNSProviderConfig(config)
-
- if assert.NoError(t, err) {
- err = provider.CleanUp("example.com", "token", "keyAuth")
- require.NoError(t, err)
- }
+ err := provider.CleanUp("example.com", "token", "keyAuth")
+ require.NoError(t, err)
}
func TestLiveNewDNSProvider_ValidEnv(t *testing.T) {
@@ -99,99 +109,59 @@ func TestLiveCleanUp(t *testing.T) {
require.NoError(t, err)
}
-func setupTest(t *testing.T) *Config {
- t.Helper()
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.APIUser = "testUser"
+ config.APIKey = "testKey"
+ config.HTTPClient = server.Client()
+ config.BaseURL = server.URL + "/v2.0/tokens"
- dnsAPI := httptest.NewServer(dnsHandler())
- t.Cleanup(dnsAPI.Close)
+ return NewDNSProviderConfig(config)
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ ).
+ Route("POST /v2.0/tokens",
+ http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ apiURL := fmt.Sprintf("http://%s/123456", req.Context().Value(http.LocalAddrContextKey))
- identityAPI := httptest.NewServer(identityHandler(dnsAPI.URL + "/123456"))
- t.Cleanup(identityAPI.Close)
-
- config := NewDefaultConfig()
- config.APIUser = "testUser"
- config.APIKey = "testKey"
- config.HTTPClient = identityAPI.Client()
- config.BaseURL = identityAPI.URL + "/"
-
- return config
+ resp := strings.Replace(`
+{
+ "access": {
+ "token": {
+ "id": "testToken",
+ "expires": "1970-01-01T00:00:00.000Z",
+ "tenant": {
+ "id": "123456",
+ "name": "123456"
+ },
+ "RAX-AUTH:authenticatedBy": [
+ "APIKEY"
+ ]
+ },
+ "serviceCatalog": [
+ {
+ "type": "rax:dns",
+ "endpoints": [
+ {
+ "publicURL": "https://dns.api.rackspacecloud.com/v1.0/123456",
+ "tenantId": "123456"
+ }
+ ],
+ "name": "cloudDNS"
+ }
+ ],
+ "user": {
+ "id": "fakeUseID",
+ "name": "testUser"
+ }
+ }
}
+`, "https://dns.api.rackspacecloud.com/v1.0/123456", apiURL, 1)
-func identityHandler(dnsEndpoint string) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- reqBody, err := io.ReadAll(r.Body)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if string(bytes.TrimSpace(reqBody)) != `{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}` {
- http.Error(w, fmt.Sprintf("invalid body: %s", string(reqBody)), http.StatusBadRequest)
- return
- }
-
- resp := strings.Replace(identityResponseMock, "https://dns.api.rackspacecloud.com/v1.0/123456", dnsEndpoint, 1)
- w.WriteHeader(http.StatusOK)
- _, _ = fmt.Fprint(w, resp)
- })
-}
-
-func dnsHandler() *http.ServeMux {
- mux := http.NewServeMux()
-
- // Used by `getHostedZoneID()` finding `zoneID` "?name=example.com"
- mux.HandleFunc("/123456/domains", func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Query().Get("name") == "example.com" {
- w.WriteHeader(http.StatusOK)
- _, _ = fmt.Fprint(w, zoneDetailsMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/123456/domains/112233/records", func(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- // Used by `Present()` creating the TXT record
- case http.MethodPost:
- reqBody, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if string(bytes.TrimSpace(reqBody)) != `{"records":[{"name":"_acme-challenge.example.com","type":"TXT","data":"pW9ZKG0xz_PCriK-nCMOjADy9eJcgGWIzkkj2fN4uZM","ttl":300}]}` {
- http.Error(w, fmt.Sprintf("invalid body: %s", string(reqBody)), http.StatusBadRequest)
- return
- }
-
- w.WriteHeader(http.StatusAccepted)
- _, _ = fmt.Fprint(w, recordResponseMock)
-
- // Used by `findTxtRecord()` finding `record.ID` "?type=TXT&name=_acme-challenge.example.com"
- case http.MethodGet:
- if r.URL.Query().Get("type") == "TXT" && r.URL.Query().Get("name") == "_acme-challenge.example.com" {
- w.WriteHeader(http.StatusOK)
- _, _ = fmt.Fprint(w, recordDetailsMock)
- return
- }
-
- w.WriteHeader(http.StatusBadRequest)
- return
-
- // Used by `CleanUp()` deleting the TXT record "?id=445566"
- case http.MethodDelete:
- if r.URL.Query().Get("id") == "TXT-654321" {
- w.WriteHeader(http.StatusOK)
- _, _ = fmt.Fprint(w, recordDeleteMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- }
- })
-
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- http.Error(w, fmt.Sprintf("Not Found for Request: (%+v)", r), http.StatusNotFound)
- })
-
- return mux
+ rw.WriteHeader(http.StatusOK)
+ _, _ = fmt.Fprint(rw, resp)
+ }),
+ servermock.CheckRequestJSONBody(`{"auth":{"RAX-KSKEY:apiKeyCredentials":{"username":"testUser","apiKey":"testKey"}}}`))
}
diff --git a/providers/dns/rainyun/internal/client_test.go b/providers/dns/rainyun/internal/client_test.go
index 1652bba39..8246001af 100644
--- a/providers/dns/rainyun/internal/client_test.go
+++ b/providers/dns/rainyun/internal/client_test.go
@@ -1,58 +1,39 @@
package internal
import (
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders())
}
func TestClient_ListDomains(t *testing.T) {
- client := setupTest(t, "GET /domain", http.StatusOK, "domains.json")
+ client := mockBuilder().
+ Route("GET /domain",
+ servermock.ResponseFromFixture("domains.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("options", `{"columnFilters":{"domains.Domain":""},"sort":[],"page":1,"perPage":100}`)).
+ Build(t)
domains, err := client.ListDomains(t.Context())
require.NoError(t, err)
@@ -66,7 +47,11 @@ func TestClient_ListDomains(t *testing.T) {
}
func TestClient_ListDomains_error(t *testing.T) {
- client := setupTest(t, "GET /domain", http.StatusForbidden, "error.json")
+ client := mockBuilder().
+ Route("GET /domain",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusForbidden)).
+ Build(t)
_, err := client.ListDomains(t.Context())
require.Error(t, err)
@@ -75,7 +60,13 @@ func TestClient_ListDomains_error(t *testing.T) {
}
func TestClient_ListRecords(t *testing.T) {
- client := setupTest(t, "GET /domain/123/dns", http.StatusOK, "records.json")
+ client := mockBuilder().
+ Route("GET /domain/123/dns",
+ servermock.ResponseFromFixture("records.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("limit", "100").
+ With("page_no", "1")).
+ Build(t)
records, err := client.ListRecords(t.Context(), 123)
require.NoError(t, err)
@@ -103,7 +94,11 @@ func TestClient_ListRecords(t *testing.T) {
}
func TestClient_ListRecords_error(t *testing.T) {
- client := setupTest(t, "GET /domain/123/dns", http.StatusForbidden, "error.json")
+ client := mockBuilder().
+ Route("GET /domain/123/dns",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusForbidden)).
+ Build(t)
_, err := client.ListRecords(t.Context(), 123)
require.Error(t, err)
@@ -112,7 +107,9 @@ func TestClient_ListRecords_error(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "POST /domain/123/dns", http.StatusOK, "")
+ client := mockBuilder().
+ Route("POST /domain/123/dns", nil).
+ Build(t)
record := Record{
Host: "_acme-challenge.foo.example.com",
@@ -127,7 +124,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "POST /domain/123/dns", http.StatusForbidden, "error.json")
+ client := mockBuilder().
+ Route("POST /domain/123/dns",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusForbidden)).
+ Build(t)
record := Record{
Host: "_acme-challenge.foo.example.com",
@@ -144,14 +145,20 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "DELETE /domain/123/dns", http.StatusOK, "")
+ client := mockBuilder().
+ Route("DELETE /domain/123/dns", nil).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "DELETE /domain/123/dns", http.StatusForbidden, "error.json")
+ client := mockBuilder().
+ Route("DELETE /domain/123/dns",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusForbidden)).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.Error(t, err)
diff --git a/providers/dns/rcodezero/internal/client_test.go b/providers/dns/rcodezero/internal/client_test.go
index 0b54fa97f..b70107072 100644
--- a/providers/dns/rcodezero/internal/client_test.go
+++ b/providers/dns/rcodezero/internal/client_test.go
@@ -1,68 +1,30 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- apiToken := req.Header.Get(authorizationHeader)
- if apiToken != "Bearer secret" {
- http.Error(rw, fmt.Sprintf("invalid credentials: %s", apiToken), http.StatusBadRequest)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("secret")
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
- return client
+ return client, nil
}
func TestClient_UpdateRecords_error(t *testing.T) {
- client := setupTest(t, http.MethodPatch, "/v1/acme/zones/example.org/rrsets", http.StatusUnprocessableEntity, "error.json")
+ client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()).
+ Route("PATCH /v1/acme/zones/example.org/rrsets",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnprocessableEntity)).
+ Build(t)
rrSet := []UpdateRRSet{{
Name: "acme.example.org.",
@@ -77,7 +39,10 @@ func TestClient_UpdateRecords_error(t *testing.T) {
}
func TestClient_UpdateRecords(t *testing.T) {
- client := setupTest(t, http.MethodPatch, "/v1/acme/zones/example.org/rrsets", http.StatusOK, "rrsets-response.json")
+ client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()).
+ Route("PATCH /v1/acme/zones/example.org/rrsets",
+ servermock.ResponseFromFixture("rrsets-response.json")).
+ Build(t)
rrSet := []UpdateRRSet{{
Name: "acme.example.org.",
diff --git a/providers/dns/regru/internal/client_test.go b/providers/dns/regru/internal/client_test.go
index 4b4a9c8f4..0779f0d5f 100644
--- a/providers/dns/regru/internal/client_test.go
+++ b/providers/dns/regru/internal/client_test.go
@@ -1,60 +1,59 @@
package internal
import (
- "net/http"
+ "net/http/httptest"
"net/url"
- "os"
"testing"
- "time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-const (
- noopBaseURL = "https://api.reg.ru/api/regru2/nop"
- officialTestUser = "test"
- officialTestPassword = "test"
-)
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.baseURL, _ = url.Parse(server.URL)
+
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ )
+}
func TestRemoveRecord(t *testing.T) {
- // TODO(ldez): remove skip when the reg.ru API will be fixed.
- t.Skip("there is a bug with the reg.ru API: INTERNAL_API_ERROR: Внутренняя ошибка, status code: 503")
-
- client := NewClient(officialTestUser, officialTestPassword)
- client.HTTPClient = &http.Client{Timeout: 30 * time.Second}
+ client := mockBuilder().
+ Route("POST /zone/remove_record",
+ servermock.ResponseFromFixture("remove_record.json"),
+ servermock.CheckForm().Strict().
+ With("input_data", `{"domains":[{"dname":"test.ru"}],"subdomain":"_acme-challenge","content":"txttxttxt","record_type":"TXT","output_content_type":"plain"}`).
+ With("username", "user").
+ With("password", "secret").
+ With("input_format", "json")).
+ Build(t)
err := client.RemoveTxtRecord(t.Context(), "test.ru", "_acme-challenge", "txttxttxt")
require.NoError(t, err)
}
func TestRemoveRecord_errors(t *testing.T) {
- // TODO(ldez): remove skip when the reg.ru API will be fixed.
- if os.Getenv("CI") == "true" {
- t.Skip("there is a bug with the reg.ru and GitHub action: dial tcp 194.58.116.30:443: i/o timeout")
- }
-
testCases := []struct {
desc string
domain string
- username string
- password string
- baseURL string
+ response string
expected string
}{
{
desc: "authentication failed",
domain: "test.ru",
- username: "",
- password: "",
- baseURL: noopBaseURL,
+ response: "remove_record_error_auth.json",
expected: "API error: NO_AUTH: No authorization mechanism selected",
},
{
desc: "domain error",
domain: "",
- username: officialTestUser,
- password: officialTestPassword,
- baseURL: defaultBaseURL,
+ response: "remove_record_error_domain.json",
expected: "API error: NO_DOMAIN: domain_name not given or empty",
},
}
@@ -63,9 +62,9 @@ func TestRemoveRecord_errors(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client := NewClient(test.username, test.username)
- client.HTTPClient = &http.Client{Timeout: 30 * time.Second}
- client.baseURL, _ = url.Parse(test.baseURL)
+ client := mockBuilder().
+ Route("POST /zone/remove_record", servermock.ResponseFromFixture(test.response)).
+ Build(t)
err := client.RemoveTxtRecord(t.Context(), test.domain, "_acme-challenge", "txttxttxt")
require.EqualError(t, err, test.expected)
@@ -74,44 +73,37 @@ func TestRemoveRecord_errors(t *testing.T) {
}
func TestAddTXTRecord(t *testing.T) {
- // TODO(ldez): remove skip when the reg.ru API will be fixed.
- t.Skip("there is a bug with the reg.ru API: INTERNAL_API_ERROR: Внутренняя ошибка, status code: 503")
-
- client := NewClient(officialTestUser, officialTestPassword)
- client.HTTPClient = &http.Client{Timeout: 30 * time.Second}
+ client := mockBuilder().
+ Route("POST /zone/add_txt",
+ servermock.ResponseFromFixture("add_txt_record.json"),
+ servermock.CheckForm().Strict().
+ With("input_data", `{"domains":[{"dname":"test.ru"}],"subdomain":"_acme-challenge","text":"txttxttxt","output_content_type":"plain"}`).
+ With("username", "user").
+ With("password", "secret").
+ With("input_format", "json")).
+ Build(t)
err := client.AddTXTRecord(t.Context(), "test.ru", "_acme-challenge", "txttxttxt")
require.NoError(t, err)
}
func TestAddTXTRecord_errors(t *testing.T) {
- // TODO(ldez): remove skip when the reg.ru API will be fixed.
- if os.Getenv("CI") == "true" {
- t.Skip("there is a bug with the reg.ru and GitHub action: dial tcp 194.58.116.30:443: i/o timeout")
- }
-
testCases := []struct {
desc string
domain string
- username string
- password string
- baseURL string
+ response string
expected string
}{
{
desc: "authentication failed",
domain: "test.ru",
- username: "",
- password: "",
- baseURL: noopBaseURL,
+ response: "add_txt_record_error_auth.json",
expected: "API error: NO_AUTH: No authorization mechanism selected",
},
{
desc: "domain error",
domain: "",
- username: officialTestUser,
- password: officialTestPassword,
- baseURL: defaultBaseURL,
+ response: "add_txt_record_error_domain.json",
expected: "API error: NO_DOMAIN: domain_name not given or empty",
},
}
@@ -120,9 +112,9 @@ func TestAddTXTRecord_errors(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client := NewClient(test.username, test.username)
- client.HTTPClient = &http.Client{Timeout: 30 * time.Second}
- client.baseURL, _ = url.Parse(test.baseURL)
+ client := mockBuilder().
+ Route("POST /zone/add_txt", servermock.ResponseFromFixture(test.response)).
+ Build(t)
err := client.AddTXTRecord(t.Context(), test.domain, "_acme-challenge", "txttxttxt")
require.EqualError(t, err, test.expected)
diff --git a/providers/dns/regru/internal/fixtures/add_txt_record.json b/providers/dns/regru/internal/fixtures/add_txt_record.json
new file mode 100644
index 000000000..06306b4c4
--- /dev/null
+++ b/providers/dns/regru/internal/fixtures/add_txt_record.json
@@ -0,0 +1,14 @@
+{
+ "answer": {
+ "domains": [
+ {
+ "dname": "test.ru",
+ "result": "success",
+ "service_id": 12345
+ }
+ ]
+ },
+ "charset": "utf-8",
+ "messagestore": null,
+ "result": "success"
+}
diff --git a/providers/dns/regru/internal/fixtures/add_txt_record_error_auth.json b/providers/dns/regru/internal/fixtures/add_txt_record_error_auth.json
new file mode 100644
index 000000000..2d5314bf3
--- /dev/null
+++ b/providers/dns/regru/internal/fixtures/add_txt_record_error_auth.json
@@ -0,0 +1,10 @@
+{
+ "charset": "utf-8",
+ "error_code": "NO_AUTH",
+ "error_params": {
+ "command_name": "nop/zone/add_txt"
+ },
+ "error_text": "No authorization mechanism selected",
+ "messagestore": null,
+ "result": "error"
+}
diff --git a/providers/dns/regru/internal/fixtures/add_txt_record_error_domain.json b/providers/dns/regru/internal/fixtures/add_txt_record_error_domain.json
new file mode 100644
index 000000000..305846ed1
--- /dev/null
+++ b/providers/dns/regru/internal/fixtures/add_txt_record_error_domain.json
@@ -0,0 +1,14 @@
+{
+ "answer": {
+ "domains": [
+ {
+ "error_code": "NO_DOMAIN",
+ "error_text": "domain_name not given or empty",
+ "result": "error"
+ }
+ ]
+ },
+ "charset": "utf-8",
+ "messagestore": null,
+ "result": "success"
+}
diff --git a/providers/dns/regru/internal/fixtures/remove_record.json b/providers/dns/regru/internal/fixtures/remove_record.json
new file mode 100644
index 000000000..06306b4c4
--- /dev/null
+++ b/providers/dns/regru/internal/fixtures/remove_record.json
@@ -0,0 +1,14 @@
+{
+ "answer": {
+ "domains": [
+ {
+ "dname": "test.ru",
+ "result": "success",
+ "service_id": 12345
+ }
+ ]
+ },
+ "charset": "utf-8",
+ "messagestore": null,
+ "result": "success"
+}
diff --git a/providers/dns/regru/internal/fixtures/remove_record_error_auth.json b/providers/dns/regru/internal/fixtures/remove_record_error_auth.json
new file mode 100644
index 000000000..98c429c53
--- /dev/null
+++ b/providers/dns/regru/internal/fixtures/remove_record_error_auth.json
@@ -0,0 +1,10 @@
+{
+ "charset" : "utf-8",
+ "error_code" : "NO_AUTH",
+ "error_params" : {
+ "command_name" : "nop/zone/remove_record"
+ },
+ "error_text" : "No authorization mechanism selected",
+ "messagestore" : null,
+ "result" : "error"
+}
diff --git a/providers/dns/regru/internal/fixtures/remove_record_error_domain.json b/providers/dns/regru/internal/fixtures/remove_record_error_domain.json
new file mode 100644
index 000000000..a9ca88ff7
--- /dev/null
+++ b/providers/dns/regru/internal/fixtures/remove_record_error_domain.json
@@ -0,0 +1,14 @@
+{
+ "answer" : {
+ "domains" : [
+ {
+ "error_code" : "NO_DOMAIN",
+ "error_text" : "domain_name not given or empty",
+ "result" : "error"
+ }
+ ]
+ },
+ "charset" : "utf-8",
+ "messagestore" : null,
+ "result" : "success"
+}
diff --git a/providers/dns/regru/internal/readme.md b/providers/dns/regru/internal/readme.md
new file mode 100644
index 000000000..5f13012d2
--- /dev/null
+++ b/providers/dns/regru/internal/readme.md
@@ -0,0 +1,6 @@
+Test account (with the default endpoint):
+- user: `test`
+- password: `test`
+
+Noop endpoint:
+- https://api.reg.ru/api/regru2/nop
diff --git a/providers/dns/route53/fixtures/changeResourceRecordSetsResponse.xml b/providers/dns/route53/fixtures/changeResourceRecordSetsResponse.xml
new file mode 100644
index 000000000..68dba580f
--- /dev/null
+++ b/providers/dns/route53/fixtures/changeResourceRecordSetsResponse.xml
@@ -0,0 +1,8 @@
+
+
+
+ /change/123456
+ PENDING
+ 2016-02-10T01:36:41.958Z
+
+
diff --git a/providers/dns/route53/fixtures/getChangeResponse.xml b/providers/dns/route53/fixtures/getChangeResponse.xml
new file mode 100644
index 000000000..f22c09460
--- /dev/null
+++ b/providers/dns/route53/fixtures/getChangeResponse.xml
@@ -0,0 +1,8 @@
+
+
+
+ 123456
+ INSYNC
+ 2016-02-10T01:36:41.958Z
+
+
diff --git a/providers/dns/route53/fixtures/listHostedZonesByNameResponse.xml b/providers/dns/route53/fixtures/listHostedZonesByNameResponse.xml
new file mode 100644
index 000000000..db47ba1e1
--- /dev/null
+++ b/providers/dns/route53/fixtures/listHostedZonesByNameResponse.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ /hostedzone/ABCDEFG
+ example.com.
+ D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A
+
+ Test comment
+ false
+
+ 10
+
+
+ true
+ example2.com
+ ZLT12321321124
+ 1
+
diff --git a/providers/dns/route53/fixtures_test.go b/providers/dns/route53/fixtures_test.go
deleted file mode 100644
index 444a88003..000000000
--- a/providers/dns/route53/fixtures_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package route53
-
-const ChangeResourceRecordSetsResponse = `
-
-
- /change/123456
- PENDING
- 2016-02-10T01:36:41.958Z
-
-`
-
-const ListHostedZonesByNameResponse = `
-
-
-
- /hostedzone/ABCDEFG
- example.com.
- D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A
-
- Test comment
- false
-
- 10
-
-
- true
- example2.com
- ZLT12321321124
- 1
-`
-
-const GetChangeResponse = `
-
-
- 123456
- INSYNC
- 2016-02-10T01:36:41.958Z
-
-`
diff --git a/providers/dns/route53/mock_test.go b/providers/dns/route53/mock_test.go
deleted file mode 100644
index 022767385..000000000
--- a/providers/dns/route53/mock_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package route53
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-
- "github.com/stretchr/testify/require"
-)
-
-// MockResponse represents a predefined response used by a mock server.
-type MockResponse struct {
- StatusCode int
- Body string
-}
-
-// MockResponseMap maps request paths to responses.
-type MockResponseMap map[string]MockResponse
-
-func setupTest(t *testing.T, responses MockResponseMap) string {
- t.Helper()
-
- handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- path := r.URL.Path
- resp, ok := responses[path]
- if !ok {
- resp, ok = responses[r.RequestURI]
- if !ok {
- msg := fmt.Sprintf("Requested path not found in response map: %s", path)
- require.FailNow(t, msg)
- }
- }
-
- w.Header().Set("Content-Type", "application/xml")
- w.WriteHeader(resp.StatusCode)
- _, err := w.Write([]byte(resp.Body))
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- time.Sleep(100 * time.Millisecond)
-
- return server.URL
-}
diff --git a/providers/dns/route53/route53_test.go b/providers/dns/route53/route53_test.go
index 60901de6d..6079bb4e6 100644
--- a/providers/dns/route53/route53_test.go
+++ b/providers/dns/route53/route53_test.go
@@ -1,6 +1,7 @@
package route53
import (
+ "net/http/httptest"
"os"
"testing"
"time"
@@ -10,6 +11,7 @@ import (
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -30,22 +32,6 @@ var envTest = tester.NewEnvTest(
WithDomain(envDomain).
WithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain)
-func makeTestProvider(t *testing.T, serverURL string) *DNSProvider {
- t.Helper()
-
- cfg := aws.Config{
- Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "),
- Region: "mock-region",
- BaseEndpoint: aws.String(serverURL),
- RetryMaxAttempts: 1,
- }
-
- return &DNSProvider{
- client: route53.NewFromConfig(cfg),
- config: NewDefaultConfig(),
- }
-}
-
func Test_loadCredentials_FromEnv(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
@@ -154,21 +140,42 @@ func TestNewDefaultConfig(t *testing.T) {
}
func TestDNSProvider_Present(t *testing.T) {
- mockResponses := MockResponseMap{
- "/2013-04-01/hostedzonesbyname": {StatusCode: 200, Body: ListHostedZonesByNameResponse},
- "/2013-04-01/hostedzone/ABCDEFG/rrset": {StatusCode: 200, Body: ChangeResourceRecordSetsResponse},
- "/2013-04-01/change/123456": {StatusCode: 200, Body: GetChangeResponse},
- "/2013-04-01/hostedzone/ABCDEFG/rrset?name=_acme-challenge.example.com.&type=TXT": {
- StatusCode: 200,
- Body: "",
- },
- }
-
- serverURL := setupTest(t, mockResponses)
-
defer envTest.RestoreEnv()
envTest.ClearEnv()
- provider := makeTestProvider(t, serverURL)
+
+ provider := servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ cfg := aws.Config{
+ Credentials: credentials.NewStaticCredentialsProvider("abc", "123", " "),
+ Region: "mock-region",
+ BaseEndpoint: aws.String(server.URL),
+ RetryMaxAttempts: 1,
+ }
+
+ return &DNSProvider{
+ client: route53.NewFromConfig(cfg),
+ config: NewDefaultConfig(),
+ }, nil
+ },
+ ).
+ Route("GET /2013-04-01/hostedzonesbyname",
+ servermock.ResponseFromFixture("listHostedZonesByNameResponse.xml").
+ WithHeader("Content-Type", "application/xml"),
+ servermock.CheckQueryParameter().Strict().
+ With("dnsname", "example.com")).
+ Route("POST /2013-04-01/hostedzone/ABCDEFG/rrset",
+ servermock.ResponseFromFixture("changeResourceRecordSetsResponse.xml").
+ WithHeader("Content-Type", "application/xml")).
+ Route("GET /2013-04-01/change/123456",
+ servermock.ResponseFromFixture("getChangeResponse.xml").
+ WithHeader("Content-Type", "application/xml")).
+ Route("GET /2013-04-01/hostedzone/ABCDEFG/rrset",
+ servermock.Noop().
+ WithHeader("Content-Type", "application/xml"),
+ servermock.CheckQueryParameter().Strict().
+ With("name", "_acme-challenge.example.com.").
+ With("type", "TXT")).
+ Build(t)
domain := "example.com"
keyAuth := "123456d=="
diff --git a/providers/dns/safedns/internal/client_test.go b/providers/dns/safedns/internal/client_test.go
index c00a8b5a7..117a85a9f 100644
--- a/providers/dns/safedns/internal/client_test.go
+++ b/providers/dns/safedns/internal/client_test.go
@@ -1,74 +1,36 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "strings"
"testing"
"github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("secret")
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestClient_AddRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/example.com/records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- if req.Header.Get(authorizationHeader) != "secret" {
- http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized)
- return
- }
-
- reqBody, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- expectedReqBody := `{"name":"_acme-challenge.example.com","type":"TXT","content":"\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"","ttl":120}`
- if strings.TrimSpace(string(reqBody)) != expectedReqBody {
- http.Error(rw, `{"message":"invalid request"}`, http.StatusBadRequest)
- return
- }
-
- resp := `{
- "data": {
- "id": 1234567
- },
- "meta": {
- "location": "https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567"
- }
- }`
-
- rw.WriteHeader(http.StatusCreated)
- _, err = fmt.Fprint(rw, resp)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /zones/example.com/records",
+ servermock.ResponseFromFixture("add_record.json").
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBodyFromFile("add_record-request.json")).
+ Build(t)
record := Record{
Name: "_acme-challenge.example.com",
@@ -96,23 +58,42 @@ func TestClient_AddRecord(t *testing.T) {
assert.Equal(t, expected, response)
}
+func TestClient_AddRecord_error(t *testing.T) {
+ client := mockBuilder().
+ Route("POST /zones/example.com/records",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
+
+ record := Record{
+ Name: "_acme-challenge.example.com",
+ Type: "TXT",
+ Content: `"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI"`,
+ TTL: dns01.DefaultTTL,
+ }
+
+ _, err := client.AddRecord(t.Context(), "example.com", record)
+ require.EqualError(t, err, "add record: [status code: 401] Unauthenticated")
+}
+
func TestClient_RemoveRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/zones/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- if req.Header.Get(authorizationHeader) != "secret" {
- http.Error(rw, `{"message":"Unauthenticated"}`, http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(http.StatusNoContent)
- })
+ client := mockBuilder().
+ Route("DELETE /zones/example.com/records/1234567",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.RemoveRecord(t.Context(), "example.com", 1234567)
require.NoError(t, err)
}
+
+func TestClient_RemoveRecord_error(t *testing.T) {
+ client := mockBuilder().
+ Route("DELETE /zones/example.com/records/1234567",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
+
+ err := client.RemoveRecord(t.Context(), "example.com", 1234567)
+ require.EqualError(t, err, "remove record: [status code: 401] Unauthenticated")
+}
diff --git a/providers/dns/safedns/internal/fixtures/add_record-request.json b/providers/dns/safedns/internal/fixtures/add_record-request.json
new file mode 100644
index 000000000..71c8813f2
--- /dev/null
+++ b/providers/dns/safedns/internal/fixtures/add_record-request.json
@@ -0,0 +1,6 @@
+{
+ "name": "_acme-challenge.example.com",
+ "type": "TXT",
+ "content": "\"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI\"",
+ "ttl": 120
+}
diff --git a/providers/dns/safedns/internal/fixtures/add_record.json b/providers/dns/safedns/internal/fixtures/add_record.json
new file mode 100644
index 000000000..f3c4ad883
--- /dev/null
+++ b/providers/dns/safedns/internal/fixtures/add_record.json
@@ -0,0 +1,8 @@
+{
+ "data": {
+ "id": 1234567
+ },
+ "meta": {
+ "location": "https://api.ukfast.io/safedns/v1/zones/example.com/records/1234567"
+ }
+}
diff --git a/providers/dns/safedns/internal/fixtures/error.json b/providers/dns/safedns/internal/fixtures/error.json
new file mode 100644
index 000000000..47fb5916c
--- /dev/null
+++ b/providers/dns/safedns/internal/fixtures/error.json
@@ -0,0 +1,3 @@
+{
+ "message": "Unauthenticated"
+}
diff --git a/providers/dns/selectelv2/selectelv2.go b/providers/dns/selectelv2/selectelv2.go
index 19e352d7f..ca0a9107d 100644
--- a/providers/dns/selectelv2/selectelv2.go
+++ b/providers/dns/selectelv2/selectelv2.go
@@ -124,15 +124,15 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
// Timeout returns the Timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
-func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
- return p.config.PropagationTimeout, p.config.PollingInterval
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
}
// Present creates a TXT record to fulfill DNS-01 challenge.
-func (p *DNSProvider) Present(domain, _, keyAuth string) error {
+func (d *DNSProvider) Present(domain, _, keyAuth string) error {
ctx := context.Background()
- client, err := p.authorize()
+ client, err := d.authorize()
if err != nil {
return fmt.Errorf("selectelv2: authorize: %w", err)
}
@@ -153,7 +153,7 @@ func (p *DNSProvider) Present(domain, _, keyAuth string) error {
newRRSet := &selectelapi.RRSet{
Name: info.EffectiveFQDN,
Type: selectelapi.TXT,
- TTL: p.config.TTL,
+ TTL: d.config.TTL,
Records: []selectelapi.RecordItem{{Content: fmt.Sprintf("%q", info.Value)}},
}
@@ -176,10 +176,10 @@ func (p *DNSProvider) Present(domain, _, keyAuth string) error {
}
// CleanUp removes a TXT record used for DNS-01 challenge.
-func (p *DNSProvider) CleanUp(domain, _, keyAuth string) error {
+func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
ctx := context.Background()
- client, err := p.authorize()
+ client, err := d.authorize()
if err != nil {
return fmt.Errorf("selectelv2: authorize: %w", err)
}
@@ -220,8 +220,8 @@ func (p *DNSProvider) CleanUp(domain, _, keyAuth string) error {
return nil
}
-func (p *DNSProvider) authorize() (*clientWrapper, error) {
- token, err := obtainOpenstackToken(p.config)
+func (d *DNSProvider) authorize() (*clientWrapper, error) {
+ token, err := obtainOpenstackToken(d.config)
if err != nil {
return nil, err
}
@@ -230,7 +230,7 @@ func (p *DNSProvider) authorize() (*clientWrapper, error) {
extraHeaders.Set(tokenHeader, token)
return &clientWrapper{
- DNSClient: p.baseClient.WithHeaders(extraHeaders),
+ DNSClient: d.baseClient.WithHeaders(extraHeaders),
}, nil
}
diff --git a/providers/dns/selfhostde/internal/client_test.go b/providers/dns/selfhostde/internal/client_test.go
index 88f627b02..22949728c 100644
--- a/providers/dns/selfhostde/internal/client_test.go
+++ b/providers/dns/selfhostde/internal/client_test.go
@@ -1,64 +1,41 @@
package internal
import (
- "fmt"
"net/http"
"net/http/httptest"
- "net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client := NewClient("user", "secret")
- serverURL, err := url.Parse(server.URL)
- require.NoError(t, err)
+ client.baseURL = server.URL
+ client.HTTPClient = server.Client()
- client.baseURL = serverURL.String()
-
- return client, mux
+ return client, nil
}
func TestClient_UpdateTXTRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("GET /", func(rw http.ResponseWriter, req *http.Request) {
- query := req.URL.Query()
-
- fields := map[string]string{
- "username": "user",
- "password": "secret",
- "rid": "123456",
- "content": "txt",
- }
-
- for k, v := range fields {
- value := query.Get(k)
- if value != v {
- http.Error(rw, fmt.Sprintf("%s: unexpected value: %s (%s)", k, value, v), http.StatusBadRequest)
- return
- }
- }
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", nil, servermock.CheckQueryParameter().Strict().
+ With("rid", "123456").
+ With("content", "txt").
+ With("username", "user").
+ With("password", "secret"),
+ ).
+ Build(t)
err := client.UpdateTXTRecord(t.Context(), "123456", "txt")
require.NoError(t, err)
}
func TestClient_UpdateTXTRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("GET /", func(rw http.ResponseWriter, _ *http.Request) {
- http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
- })
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /", servermock.Noop().WithStatusCode(http.StatusBadRequest)).
+ Build(t)
err := client.UpdateTXTRecord(t.Context(), "123456", "txt")
- require.Error(t, err)
+ require.EqualError(t, err, "unexpected status code: [status code: 400] body: ")
}
diff --git a/providers/dns/servercow/internal/client_test.go b/providers/dns/servercow/internal/client_test.go
index b171b6408..2092bf907 100644
--- a/providers/dns/servercow/internal/client_test.go
+++ b/providers/dns/servercow/internal/client_test.go
@@ -2,53 +2,35 @@ package internal
import (
"encoding/json"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("", "")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With("X-Auth-Username", "user").
+ With("X-Auth-Password", "secret"),
+ )
}
func TestClient_GetRecords(t *testing.T) {
- client, handler := setupTest(t)
-
- handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- file, err := os.Open("./fixtures/records-01.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /lego.wtf", servermock.ResponseFromFixture("records-01.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "lego.wtf")
require.NoError(t, err)
@@ -63,20 +45,9 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_error(t *testing.T) {
- client, handler := setupTest(t)
-
- handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "authentication failed"})
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /lego.wtf", servermock.JSONEncode(Message{ErrorMsg: "authentication failed"})).
+ Build(t)
records, err := client.GetRecords(t.Context(), "lego.wtf")
require.Error(t, err)
@@ -85,33 +56,11 @@ func TestClient_GetRecords_error(t *testing.T) {
}
func TestClient_CreateUpdateRecord(t *testing.T) {
- client, handler := setupTest(t)
-
- handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- content, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- expectedRequest := `{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":["aaa","bbb"]}`
-
- if !assert.JSONEq(t, expectedRequest, string(content)) {
- http.Error(rw, "invalid content", http.StatusBadRequest)
- return
- }
-
- err = json.NewEncoder(rw).Encode(Message{Message: "ok"})
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /lego.wtf",
+ servermock.JSONEncode(Message{Message: "ok"}),
+ servermock.CheckRequestJSONBody(`{"name":"_acme-challenge.www","type":"TXT","ttl":30,"content":["aaa","bbb"]}`)).
+ Build(t)
record := Record{
Name: "_acme-challenge.www",
@@ -128,20 +77,10 @@ func TestClient_CreateUpdateRecord(t *testing.T) {
}
func TestClient_CreateUpdateRecord_error(t *testing.T) {
- client, handler := setupTest(t)
-
- handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"})
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /lego.wtf",
+ servermock.JSONEncode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"})).
+ Build(t)
record := Record{
Name: "_acme-challenge.www",
@@ -154,33 +93,11 @@ func TestClient_CreateUpdateRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, handler := setupTest(t)
-
- handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- content, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- expectedRequest := `{"name":"_acme-challenge.www","type":"TXT"}`
-
- if !assert.JSONEq(t, expectedRequest, string(content)) {
- http.Error(rw, "invalid content", http.StatusBadRequest)
- return
- }
-
- err = json.NewEncoder(rw).Encode(Message{Message: "ok"})
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("DELETE /lego.wtf",
+ servermock.JSONEncode(Message{Message: "ok"}),
+ servermock.CheckRequestJSONBody(`{"name":"_acme-challenge.www","type":"TXT"}`)).
+ Build(t)
record := Record{
Name: "_acme-challenge.www",
@@ -195,20 +112,10 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client, handler := setupTest(t)
-
- handler.HandleFunc("/lego.wtf", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- err := json.NewEncoder(rw).Encode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"})
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("DELETE /lego.wtf",
+ servermock.JSONEncode(Message{ErrorMsg: "parameter type must be cname, txt, tlsa, caa, a or aaaa"})).
+ Build(t)
record := Record{
Name: "_acme-challenge.www",
diff --git a/providers/dns/shellrent/internal/client_test.go b/providers/dns/shellrent/internal/client_test.go
index c160ddf56..7047ce835 100644
--- a/providers/dns/shellrent/internal/client_test.go
+++ b/providers/dns/shellrent/internal/client_test.go
@@ -1,68 +1,33 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get(authorizationHeader)
- if auth != "user.secret" {
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("user.secret"))
}
func TestClient_ListServices(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/purchase", http.StatusOK, "purchase.json")
+ client := mockBuilder().
+ Route("GET /purchase", servermock.ResponseFromFixture("purchase.json")).
+ Build(t)
services, err := client.ListServices(t.Context())
require.NoError(t, err)
@@ -73,21 +38,29 @@ func TestClient_ListServices(t *testing.T) {
}
func TestClient_ListServices_error(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/purchase", http.StatusOK, "error.json")
+ client := mockBuilder().
+ Route("GET /purchase", servermock.ResponseFromFixture("error.json")).
+ Build(t)
_, err := client.ListServices(t.Context())
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_ListServices_error_status(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/purchase", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("GET /purchase",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.ListServices(t.Context())
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_GetServiceDetails(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/purchase/details/123", http.StatusOK, "purchase-details.json")
+ client := mockBuilder().
+ Route("GET /purchase/details/123", servermock.ResponseFromFixture("purchase-details.json")).
+ Build(t)
services, err := client.GetServiceDetails(t.Context(), 123)
require.NoError(t, err)
@@ -98,21 +71,29 @@ func TestClient_GetServiceDetails(t *testing.T) {
}
func TestClient_GetServiceDetails_error(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/purchase/details/123", http.StatusOK, "error.json")
+ client := mockBuilder().
+ Route("GET /purchase/details/123", servermock.ResponseFromFixture("error.json")).
+ Build(t)
_, err := client.GetServiceDetails(t.Context(), 123)
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_GetServiceDetails_error_status(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/purchase/details/123", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("GET /purchase/details/123",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetServiceDetails(t.Context(), 123)
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_GetDomainDetails(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/domain/details/123", http.StatusOK, "domain-details.json")
+ client := mockBuilder().
+ Route("GET /domain/details/123", servermock.ResponseFromFixture("domain-details.json")).
+ Build(t)
services, err := client.GetDomainDetails(t.Context(), 123)
require.NoError(t, err)
@@ -123,21 +104,29 @@ func TestClient_GetDomainDetails(t *testing.T) {
}
func TestClient_GetDomainDetails_error(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/domain/details/123", http.StatusOK, "error.json")
+ client := mockBuilder().
+ Route("GET /domain/details/123", servermock.ResponseFromFixture("error.json")).
+ Build(t)
_, err := client.GetDomainDetails(t.Context(), 123)
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_GetDomainDetails_error_status(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/domain/details/123", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("GET /domain/details/123",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetDomainDetails(t.Context(), 123)
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_CreateRecord(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/dns_record/store/123", http.StatusOK, "dns_record-store.json")
+ client := mockBuilder().
+ Route("POST /dns_record/store/123", servermock.ResponseFromFixture("dns_record-store.json")).
+ Build(t)
services, err := client.CreateRecord(t.Context(), 123, Record{})
require.NoError(t, err)
@@ -148,35 +137,49 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_CreateRecord_error(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/dns_record/store/123", http.StatusOK, "error.json")
+ client := mockBuilder().
+ Route("POST /dns_record/store/123", servermock.ResponseFromFixture("error.json")).
+ Build(t)
_, err := client.CreateRecord(t.Context(), 123, Record{})
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_CreateRecord_error_status(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/dns_record/store/123", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("POST /dns_record/store/123",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.CreateRecord(t.Context(), 123, Record{})
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns_record/remove/123/456", http.StatusOK, "dns_record-remove.json")
+ client := mockBuilder().
+ Route("DELETE /dns_record/remove/123/456", servermock.ResponseFromFixture("dns_record-remove.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns_record/remove/123/456", http.StatusOK, "error.json")
+ client := mockBuilder().
+ Route("DELETE /dns_record/remove/123/456", servermock.ResponseFromFixture("error.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
}
func TestClient_DeleteRecord_error_status(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns_record/remove/123/456", http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("DELETE /dns_record/remove/123/456",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
err := client.DeleteRecord(t.Context(), 123, 456)
require.EqualError(t, err, "code 2: Token di autorizzazione non valido")
diff --git a/providers/dns/simply/internal/client_test.go b/providers/dns/simply/internal/client_test.go
index e822b03cf..83aa714bf 100644
--- a/providers/dns/simply/internal/client_test.go
+++ b/providers/dns/simply/internal/client_test.go
@@ -1,24 +1,37 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestClient_GetRecords(t *testing.T) {
- client, mux := setupTest(t)
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("accountname", "apikey")
+ if err != nil {
+ return nil, err
+ }
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodGet, http.StatusOK, "get_records.json"))
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
+
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders())
+}
+
+func TestClient_GetRecords(t *testing.T) {
+ client := mockBuilder().
+ Route("GET /accountname/apikey/my/products/azone01/dns/records",
+ servermock.ResponseFromFixture("get_records.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "azone01")
require.NoError(t, err)
@@ -62,9 +75,11 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodGet, http.StatusBadRequest, "bad_auth_error.json"))
+ client := mockBuilder().
+ Route("GET /accountname/apikey/my/products/azone01/dns/records",
+ servermock.ResponseFromFixture("bad_auth_error.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
records, err := client.GetRecords(t.Context(), "azone01")
require.Error(t, err)
@@ -73,9 +88,10 @@ func TestClient_GetRecords_error(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodPost, http.StatusOK, "add_record.json"))
+ client := mockBuilder().
+ Route("POST /accountname/apikey/my/products/azone01/dns/records",
+ servermock.ResponseFromFixture("add_record.json")).
+ Build(t)
record := Record{
Name: "arecord01",
@@ -92,9 +108,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records", mockHandler(http.MethodPost, http.StatusNotFound, "bad_zone_error.json"))
+ client := mockBuilder().
+ Route("POST /accountname/apikey/my/products/azone01/dns/records",
+ servermock.ResponseFromFixture("bad_zone_error.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
record := Record{
Name: "arecord01",
@@ -111,9 +129,10 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_EditRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodPut, http.StatusOK, "success.json"))
+ client := mockBuilder().
+ Route("PUT /accountname/apikey/my/products/azone01/dns/records/123456789",
+ servermock.ResponseFromFixture("success.json")).
+ Build(t)
record := Record{
Name: "arecord01",
@@ -128,9 +147,11 @@ func TestClient_EditRecord(t *testing.T) {
}
func TestClient_EditRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodPut, http.StatusNotFound, "invalid_record_id.json"))
+ client := mockBuilder().
+ Route("PUT /accountname/apikey/my/products/azone01/dns/records/123456789",
+ servermock.ResponseFromFixture("invalid_record_id.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
record := Record{
Name: "arecord01",
@@ -145,63 +166,22 @@ func TestClient_EditRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodDelete, http.StatusOK, "success.json"))
+ client := mockBuilder().
+ Route("DELETE /accountname/apikey/my/products/azone01/dns/records/123456789",
+ servermock.ResponseFromFixture("success.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "azone01", 123456789)
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/accountname/apikey/my/products/azone01/dns/records/123456789", mockHandler(http.MethodDelete, http.StatusNotFound, "invalid_record_id.json"))
+ client := mockBuilder().
+ Route("DELETE /accountname/apikey/my/products/azone01/dns/records/123456789",
+ servermock.ResponseFromFixture("invalid_record_id.json").
+ WithStatusCode(http.StatusNotFound)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "azone01", 123456789)
require.Error(t, err)
}
-
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client, err := NewClient("accountname", "apikey")
- require.NoError(t, err)
-
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func mockHandler(method string, statusCode int, filename string) func(http.ResponseWriter, *http.Request) {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusMethodNotAllowed)
- return
- }
-
- if filename == "" {
- rw.WriteHeader(statusCode)
- return
- }
-
- file, err := os.Open(filepath.FromSlash(path.Join("./fixtures", filename)))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-}
diff --git a/providers/dns/sonic/internal/client_test.go b/providers/dns/sonic/internal/client_test.go
index 618538780..751ccee8f 100644
--- a/providers/dns/sonic/internal/client_test.go
+++ b/providers/dns/sonic/internal/client_test.go
@@ -1,31 +1,23 @@
package internal
import (
- "fmt"
- "net/http"
"net/http/httptest"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, body string) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/host", func(rw http.ResponseWriter, req *http.Request) {
- _, _ = fmt.Fprintln(rw, body)
- })
-
+func setupClient(server *httptest.Server) (*Client, error) {
client, err := NewClient("foo", "secret")
- require.NoError(t, err)
+ if err != nil {
+ return nil, err
+ }
client.baseURL = server.URL
+ client.HTTPClient = server.Client()
- return client
+ return client, nil
}
func TestClient_SetRecord(t *testing.T) {
@@ -50,7 +42,11 @@ func TestClient_SetRecord(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- client := setupTest(t, test.response)
+ client := servermock.NewBuilder[*Client](setupClient, servermock.CheckHeader().WithJSONHeaders()).
+ Route("PUT /host",
+ servermock.RawStringResponse(test.response),
+ servermock.CheckRequestJSONBody(`{"userid":"foo","apikey":"secret","hostname":"example.com","value":"txttxttxt","ttl":10,"type":"TXT"}`)).
+ Build(t)
err := client.SetRecord(t.Context(), "example.com", "txttxttxt", 10)
test.assert(t, err)
diff --git a/providers/dns/spaceship/internal/client_test.go b/providers/dns/spaceship/internal/client_test.go
index ec6787f8e..f32843652 100644
--- a/providers/dns/spaceship/internal/client_test.go
+++ b/providers/dns/spaceship/internal/client_test.go
@@ -1,58 +1,40 @@
package internal
import (
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("key", "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if filename == "" {
- rw.WriteHeader(status)
- return
- }
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("key", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ With("X-Api-Key", "key").
+ With("X-Api-Secret", "secret"),
+ )
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "PUT /dns/records/example.com", http.StatusOK, "")
+ client := mockBuilder().
+ Route("PUT /dns/records/example.com", nil,
+ servermock.CheckRequestJSONBody(`{"items":[{"type":"TXT","name":"@","ttl":60}]}`)).
+ Build(t)
record := Record{
Type: "TXT",
@@ -65,7 +47,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "PUT /dns/records/example.com", http.StatusUnprocessableEntity, "error.json")
+ client := mockBuilder().
+ Route("PUT /dns/records/example.com",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnprocessableEntity)).
+ Build(t)
record := Record{
Type: "TXT",
@@ -78,7 +64,10 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "DELETE /dns/records/example.com", http.StatusOK, "")
+ client := mockBuilder().
+ Route("DELETE /dns/records/example.com", nil,
+ servermock.CheckRequestJSONBody(`[{"type":"TXT","name":"@","ttl":60}]`)).
+ Build(t)
record := Record{
Type: "TXT",
@@ -91,7 +80,11 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "DELETE /dns/records/example.com", http.StatusUnprocessableEntity, "error.json")
+ client := mockBuilder().
+ Route("DELETE /dns/records/example.com",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnprocessableEntity)).
+ Build(t)
record := Record{
Type: "TXT",
@@ -104,7 +97,10 @@ func TestClient_DeleteRecord_error(t *testing.T) {
}
func TestClient_GetRecords(t *testing.T) {
- client := setupTest(t, "GET /dns/records/example.com", http.StatusOK, "get-records.json")
+ client := mockBuilder().
+ Route("GET /dns/records/example.com",
+ servermock.ResponseFromFixture("get-records.json")).
+ Build(t)
records, err := client.GetRecords(t.Context(), "example.com")
require.NoError(t, err)
@@ -117,7 +113,11 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_GetRecords_error(t *testing.T) {
- client := setupTest(t, "GET /dns/records/example.com", http.StatusUnprocessableEntity, "error.json")
+ client := mockBuilder().
+ Route("GET /dns/records/example.com",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnprocessableEntity)).
+ Build(t)
_, err := client.GetRecords(t.Context(), "example.com")
require.EqualError(t, err, "^$, name: The domain name contains invalid characters")
diff --git a/providers/dns/stackpath/internal/client_test.go b/providers/dns/stackpath/internal/client_test.go
index cb56ef728..5195aa973 100644
--- a/providers/dns/stackpath/internal/client_test.go
+++ b/providers/dns/stackpath/internal/client_test.go
@@ -1,47 +1,37 @@
package internal
import (
+ "context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(context.Background(), "STACK_ID", "CLIENT_ID", "CLIENT_SECRET")
+ client.httpClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL + "/")
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(t.Context(), "STACK_ID", "CLIENT_ID", "CLIENT_SECRET")
- client.httpClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL + "/")
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders(),
+ )
}
func TestClient_GetZoneRecords(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, _ *http.Request) {
- content := `
- {
- "records": [
- {"id":"1","name":"foo1","type":"TXT","ttl":120,"data":"txtTXTtxt"},
- {"id":"2","name":"foo2","type":"TXT","ttl":121,"data":"TXTtxtTXT"}
- ]
- }`
-
- _, err := w.Write([]byte(content))
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /STACK_ID/zones/A/records",
+ servermock.ResponseFromFixture("get_zone_records.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("page_request.filter", "name='foo1' and type='TXT'")).
+ Build(t)
records, err := client.GetZoneRecords(t.Context(), "foo1", &Zone{ID: "A", Domain: "test"})
require.NoError(t, err)
@@ -55,22 +45,14 @@ func TestClient_GetZoneRecords(t *testing.T) {
}
func TestClient_GetZoneRecords_apiError(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/STACK_ID/zones/A/records", func(w http.ResponseWriter, _ *http.Request) {
- content := `
+ client := mockBuilder().
+ Route("GET /STACK_ID/zones/A/records",
+ servermock.RawStringResponse(`
{
"code": 401,
"error": "an unauthorized request is attempted."
-}`
-
- w.WriteHeader(http.StatusUnauthorized)
- _, err := w.Write([]byte(content))
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+}`).WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetZoneRecords(t.Context(), "foo1", &Zone{ID: "A", Domain: "test"})
@@ -79,47 +61,12 @@ func TestClient_GetZoneRecords_apiError(t *testing.T) {
}
func TestClient_GetZones(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/STACK_ID/zones", func(w http.ResponseWriter, _ *http.Request) {
- content := `
-{
- "pageInfo": {
- "totalCount": "5",
- "hasPreviousPage": false,
- "hasNextPage": false,
- "startCursor": "1",
- "endCursor": "1"
- },
- "zones": [
- {
- "stackId": "my_stack",
- "accountId": "my_account",
- "id": "A",
- "domain": "foo.com",
- "version": "1",
- "labels": {
- "property1": "val1",
- "property2": "val2"
- },
- "created": "2018-10-07T02:31:49Z",
- "updated": "2018-10-07T02:31:49Z",
- "nameservers": [
- "1.1.1.1"
- ],
- "verified": "2018-10-07T02:31:49Z",
- "status": "ACTIVE",
- "disabled": false
- }
- ]
-}`
-
- _, err := w.Write([]byte(content))
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("GET /STACK_ID/zones",
+ servermock.ResponseFromFixture("get_zones.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("page_request.filter", "domain='foo.com'")).
+ Build(t)
zone, err := client.GetZones(t.Context(), "sub.foo.com")
require.NoError(t, err)
diff --git a/providers/dns/stackpath/internal/fixtures/get_zone_records.json b/providers/dns/stackpath/internal/fixtures/get_zone_records.json
new file mode 100644
index 000000000..1556d08fe
--- /dev/null
+++ b/providers/dns/stackpath/internal/fixtures/get_zone_records.json
@@ -0,0 +1,6 @@
+{
+ "records": [
+ {"id":"1","name":"foo1","type":"TXT","ttl":120,"data":"txtTXTtxt"},
+ {"id":"2","name":"foo2","type":"TXT","ttl":121,"data":"TXTtxtTXT"}
+ ]
+}
diff --git a/providers/dns/stackpath/internal/fixtures/get_zones.json b/providers/dns/stackpath/internal/fixtures/get_zones.json
new file mode 100644
index 000000000..7630ef4fe
--- /dev/null
+++ b/providers/dns/stackpath/internal/fixtures/get_zones.json
@@ -0,0 +1,30 @@
+{
+ "pageInfo": {
+ "totalCount": "5",
+ "hasPreviousPage": false,
+ "hasNextPage": false,
+ "startCursor": "1",
+ "endCursor": "1"
+ },
+ "zones": [
+ {
+ "stackId": "my_stack",
+ "accountId": "my_account",
+ "id": "A",
+ "domain": "foo.com",
+ "version": "1",
+ "labels": {
+ "property1": "val1",
+ "property2": "val2"
+ },
+ "created": "2018-10-07T02:31:49Z",
+ "updated": "2018-10-07T02:31:49Z",
+ "nameservers": [
+ "1.1.1.1"
+ ],
+ "verified": "2018-10-07T02:31:49Z",
+ "status": "ACTIVE",
+ "disabled": false
+ }
+ ]
+}
diff --git a/providers/dns/technitium/internal/client_test.go b/providers/dns/technitium/internal/client_test.go
index f8b0d049b..cd6914918 100644
--- a/providers/dns/technitium/internal/client_test.go
+++ b/providers/dns/technitium/internal/client_test.go
@@ -1,50 +1,39 @@
package internal
import (
- "io"
- "net/http"
"net/http/httptest"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient(server.URL, "secret")
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient(server.URL, "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithContentTypeFromURLEncoded())
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "POST /api/zones/records/add", "add-record.json")
+ client := mockBuilder().
+ Route("POST /api/zones/records/add",
+ servermock.ResponseFromFixture("add-record.json"),
+ servermock.CheckForm().Strict().
+ With("domain", "_acme-challenge.example.com").
+ With("text", "txtTXTtxt").
+ With("type", "TXT").
+ With("token", "secret")).
+ Build(t)
record := Record{
Domain: "_acme-challenge.example.com",
@@ -61,7 +50,10 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "POST /api/zones/records/add", "error.json")
+ client := mockBuilder().
+ Route("POST /api/zones/records/add",
+ servermock.ResponseFromFixture("error.json")).
+ Build(t)
record := Record{
Domain: "_acme-challenge.example.com",
@@ -76,7 +68,15 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "POST /api/zones/records/delete", "delete-record.json")
+ client := mockBuilder().
+ Route("POST /api/zones/records/delete",
+ servermock.ResponseFromFixture("delete-record.json"),
+ servermock.CheckForm().Strict().
+ With("domain", "_acme-challenge.example.com").
+ With("text", "txtTXTtxt").
+ With("type", "TXT").
+ With("token", "secret")).
+ Build(t)
record := Record{
Domain: "_acme-challenge.example.com",
@@ -89,7 +89,10 @@ func TestClient_DeleteRecord(t *testing.T) {
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "POST /api/zones/records/delete", "error.json")
+ client := mockBuilder().
+ Route("POST /api/zones/records/delete",
+ servermock.ResponseFromFixture("error.json")).
+ Build(t)
record := Record{
Domain: "_acme-challenge.example.com",
diff --git a/providers/dns/timewebcloud/internal/client_test.go b/providers/dns/timewebcloud/internal/client_test.go
index c5a861f68..9d16ba4c5 100644
--- a/providers/dns/timewebcloud/internal/client_test.go
+++ b/providers/dns/timewebcloud/internal/client_test.go
@@ -1,86 +1,35 @@
package internal
import (
- "bytes"
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"))
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"))
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func checkAuthorizationHeader(req *http.Request) error {
- val := req.Header.Get("Authorization")
- if val != "Bearer secret" {
- return fmt.Errorf("invalid header value, got: %s want %s", val, "Bearer secret")
- }
- return nil
-}
-
-func writeResponse(rw http.ResponseWriter, statusCode int, filename string) error {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- return err
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(statusCode)
-
- _, err = io.Copy(rw, file)
- if err != nil {
- return err
- }
-
- return nil
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer secret"),
+ )
}
func TestClient_CreateRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("POST /v1/domains/example.com/dns-records", func(rw http.ResponseWriter, req *http.Request) {
- err := checkAuthorizationHeader(req)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- content, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if string(bytes.TrimSpace(content)) != `{"type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","subdomain":"_acme-challenge"}` {
- http.Error(rw, "invalid request body: "+string(content), http.StatusBadRequest)
- return
- }
-
- err = writeResponse(rw, http.StatusOK, "createDomainDNSRecord.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /v1/domains/example.com/dns-records",
+ servermock.ResponseFromFixture("createDomainDNSRecord.json"),
+ servermock.CheckRequestJSONBody(`{"type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","subdomain":"_acme-challenge"}`)).
+ Build(t)
payload := DNSRecord{
Type: "TXT",
@@ -100,15 +49,11 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_CreateRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("POST /v1/domains/example.com/dns-records", func(rw http.ResponseWriter, _ *http.Request) {
- err := writeResponse(rw, http.StatusBadRequest, "error_bad_request.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("POST /v1/domains/example.com/dns-records",
+ servermock.ResponseFromFixture("error_bad_request.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
_, err := client.CreateRecord(t.Context(), "example.com.", DNSRecord{})
require.Error(t, err)
@@ -117,32 +62,22 @@ func TestClient_CreateRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("DELETE /v1/domains/example.com/dns-records/123", func(rw http.ResponseWriter, req *http.Request) {
- err := checkAuthorizationHeader(req)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(http.StatusNoContent)
- })
+ client := mockBuilder().
+ Route("DELETE /v1/domains/example.com/dns-records/123",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com.", 123)
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("DELETE /v1/domains/example.com/dns-records/123", func(rw http.ResponseWriter, _ *http.Request) {
- err := writeResponse(rw, http.StatusBadRequest, "error_unauthorized.json")
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ client := mockBuilder().
+ Route("DELETE /v1/domains/example.com/dns-records/123",
+ servermock.ResponseFromFixture("error_unauthorized.json").
+ WithStatusCode(http.StatusBadRequest)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com.", 123)
require.Error(t, err)
diff --git a/providers/dns/variomedia/internal/client_test.go b/providers/dns/variomedia/internal/client_test.go
index 0daa64f7a..24778bdaf 100644
--- a/providers/dns/variomedia/internal/client_test.go
+++ b/providers/dns/variomedia/internal/client_test.go
@@ -1,67 +1,37 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.baseURL, _ = url.Parse(server.URL)
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient("secret")
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
-}
-
-func mockHandler(method, filename string) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("invalid method, got %s want %s", req.Method, method), http.StatusBadRequest)
- return
- }
-
- filename = "./fixtures/" + filename
- statusCode := http.StatusOK
-
- if req.Header.Get(authorizationHeader) != "token secret" {
- statusCode = http.StatusUnauthorized
- filename = "./fixtures/error.json"
- }
-
- rw.WriteHeader(statusCode)
-
- file, err := os.Open(filename)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- }
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithAccept("application/vnd.variomedia.v1+json").
+ WithAuthorization("token secret"))
}
func TestClient_CreateDNSRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/dns-records", mockHandler(http.MethodPost, "POST_dns-records.json"))
+ client := mockBuilder().
+ Route("POST /dns-records",
+ servermock.ResponseFromFixture("POST_dns-records.json"),
+ servermock.CheckHeader().
+ WithContentType("application/vnd.api+json"),
+ servermock.CheckRequestJSONBody(`{"data":{"type":"dns-record","attributes":{"record_type":"TXT","name":"_acme-challenge","domain":"example.com","data":"test","ttl":300}}}`)).
+ Build(t)
record := DNSRecord{
RecordType: "TXT",
@@ -107,9 +77,10 @@ func TestClient_CreateDNSRecord(t *testing.T) {
}
func TestClient_DeleteDNSRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/dns-records/test", mockHandler(http.MethodDelete, "DELETE_dns-records_pending.json"))
+ client := mockBuilder().
+ Route("DELETE /dns-records/test",
+ servermock.ResponseFromFixture("DELETE_dns-records_pending.json")).
+ Build(t)
resp, err := client.DeleteDNSRecord(t.Context(), "test")
require.NoError(t, err)
@@ -142,9 +113,10 @@ func TestClient_DeleteDNSRecord(t *testing.T) {
}
func TestClient_GetJob(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/queue-jobs/test", mockHandler(http.MethodGet, "GET_queue-jobs.json"))
+ client := mockBuilder().
+ Route("GET /queue-jobs/test",
+ servermock.ResponseFromFixture("GET_queue-jobs.json")).
+ Build(t)
resp, err := client.GetJob(t.Context(), "test")
require.NoError(t, err)
diff --git a/providers/dns/vegadns/fixtures/create_record.json b/providers/dns/vegadns/fixtures/create_record.json
new file mode 100644
index 000000000..2199130b9
--- /dev/null
+++ b/providers/dns/vegadns/fixtures/create_record.json
@@ -0,0 +1,12 @@
+{
+ "status": "ok",
+ "record": {
+ "name": "_acme-challenge.example.com",
+ "value": "my_challenge",
+ "record_type": "TXT",
+ "ttl": 3600,
+ "record_id": 3,
+ "location_id": null,
+ "domain_id": 1
+ }
+}
diff --git a/providers/dns/vegadns/fixtures/record_delete.json b/providers/dns/vegadns/fixtures/record_delete.json
new file mode 100644
index 000000000..bc4e01029
--- /dev/null
+++ b/providers/dns/vegadns/fixtures/record_delete.json
@@ -0,0 +1,3 @@
+{
+ "status": "ok"
+}
diff --git a/providers/dns/vegadns/fixtures/records.json b/providers/dns/vegadns/fixtures/records.json
new file mode 100644
index 000000000..9fa41ce7a
--- /dev/null
+++ b/providers/dns/vegadns/fixtures/records.json
@@ -0,0 +1,43 @@
+{
+ "status": "ok",
+ "total_records": 2,
+ "domain": {
+ "status": "active",
+ "domain": "example.com",
+ "owner_id": 0,
+ "domain_id": 1
+ },
+ "records": [
+ {
+ "retry": "2048",
+ "minimum": "2560",
+ "refresh": "16384",
+ "email": "hostmaster.example.com",
+ "record_type": "SOA",
+ "expire": "1048576",
+ "ttl": 86400,
+ "record_id": 1,
+ "nameserver": "ns1.example.com",
+ "domain_id": 1,
+ "serial": ""
+ },
+ {
+ "name": "example.com",
+ "value": "ns1.example.com",
+ "record_type": "NS",
+ "ttl": 3600,
+ "record_id": 2,
+ "location_id": null,
+ "domain_id": 1
+ },
+ {
+ "name": "_acme-challenge.example.com",
+ "value": "my_challenge",
+ "record_type": "TXT",
+ "ttl": 3600,
+ "record_id": 3,
+ "location_id": null,
+ "domain_id": 1
+ }
+ ]
+}
diff --git a/providers/dns/vegadns/fixtures/token.json b/providers/dns/vegadns/fixtures/token.json
new file mode 100644
index 000000000..39ab1a4a9
--- /dev/null
+++ b/providers/dns/vegadns/fixtures/token.json
@@ -0,0 +1,5 @@
+{
+ "access_token": "699dd4ff-e381-46b8-8bf8-5de49dd56c1f",
+ "token_type": "bearer",
+ "expires_in": 3600
+}
diff --git a/providers/dns/vegadns/vegadns_mock_test.go b/providers/dns/vegadns/vegadns_mock_test.go
deleted file mode 100644
index 5a705e092..000000000
--- a/providers/dns/vegadns/vegadns_mock_test.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package vegadns
-
-const tokenResponseMock = `
-{
- "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f",
- "token_type":"bearer",
- "expires_in":3600
-}
-`
-
-const domainsResponseMock = `
-{
- "domains":[
- {
- "domain_id":1,
- "domain":"example.com",
- "status":"active",
- "owner_id":0
- }
- ]
-}
-`
-
-const recordsResponseMock = `
-{
- "status":"ok",
- "total_records":2,
- "domain":{
- "status":"active",
- "domain":"example.com",
- "owner_id":0,
- "domain_id":1
- },
- "records":[
- {
- "retry":"2048",
- "minimum":"2560",
- "refresh":"16384",
- "email":"hostmaster.example.com",
- "record_type":"SOA",
- "expire":"1048576",
- "ttl":86400,
- "record_id":1,
- "nameserver":"ns1.example.com",
- "domain_id":1,
- "serial":""
- },
- {
- "name":"example.com",
- "value":"ns1.example.com",
- "record_type":"NS",
- "ttl":3600,
- "record_id":2,
- "location_id":null,
- "domain_id":1
- },
- {
- "name":"_acme-challenge.example.com",
- "value":"my_challenge",
- "record_type":"TXT",
- "ttl":3600,
- "record_id":3,
- "location_id":null,
- "domain_id":1
- }
- ]
-}
-`
-
-const recordCreatedResponseMock = `
-{
- "status":"ok",
- "record":{
- "name":"_acme-challenge.example.com",
- "value":"my_challenge",
- "record_type":"TXT",
- "ttl":3600,
- "record_id":3,
- "location_id":null,
- "domain_id":1
- }
-}
-`
-
-const recordDeletedResponseMock = `{"status": "ok"}`
diff --git a/providers/dns/vegadns/vegadns_test.go b/providers/dns/vegadns/vegadns_test.go
index 60f614c3b..48f54faab 100644
--- a/providers/dns/vegadns/vegadns_test.go
+++ b/providers/dns/vegadns/vegadns_test.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -28,10 +29,7 @@ func TestDNSProvider_TimeoutSuccess(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
- setupTest(t, muxSuccess())
-
- provider, err := NewDNSProvider()
- require.NoError(t, err)
+ provider := mockBuilder().Build(t)
timeout, interval := provider.Timeout()
assert.Equal(t, 12*time.Minute, timeout)
@@ -42,20 +40,38 @@ func TestDNSProvider_Present(t *testing.T) {
testCases := []struct {
desc string
handler http.Handler
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "Success",
- handler: muxSuccess(),
+ desc: "Success",
+ builder: mockBuilder().
+ Route("POST /1.0/token",
+ servermock.ResponseFromFixture("token.json")).
+ Route("GET /1.0/domains", getDomainHandler()).
+ Route("POST /1.0/records",
+ servermock.ResponseFromFixture("create_record.json").
+ WithStatusCode(http.StatusCreated)),
},
{
- desc: "FailToFindZone",
- handler: muxFailToFindZone(),
+ desc: "FailToFindZone",
+ builder: mockBuilder().
+ Route("POST /1.0/token",
+ servermock.ResponseFromFixture("token.json")).
+ Route("GET /1.0/domains",
+ servermock.Noop().
+ WithStatusCode(http.StatusNotFound)),
expectedError: "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in Present: Unable to find auth zone for fqdn _acme-challenge.example.com",
},
{
- desc: "FailToCreateTXT",
- handler: muxFailToCreateTXT(),
+ desc: "FailToCreateTXT",
+ builder: mockBuilder().
+ Route("POST /1.0/token",
+ servermock.ResponseFromFixture("token.json")).
+ Route("GET /1.0/domains", getDomainHandler()).
+ Route("POST /1.0/records",
+ servermock.Noop().
+ WithStatusCode(http.StatusBadRequest)),
expectedError: "vegadns: Got bad answer from VegaDNS on CreateTXT. Code: 400. Message: ",
},
}
@@ -65,12 +81,9 @@ func TestDNSProvider_Present(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
- setupTest(t, test.handler)
+ provider := test.builder.Build(t)
- provider, err := NewDNSProvider()
- require.NoError(t, err)
-
- err = provider.Present(testDomain, "token", "keyAuth")
+ err := provider.Present(testDomain, "token", "keyAuth")
if test.expectedError == "" {
assert.NoError(t, err)
} else {
@@ -83,21 +96,41 @@ func TestDNSProvider_Present(t *testing.T) {
func TestDNSProvider_CleanUp(t *testing.T) {
testCases := []struct {
desc string
- handler http.Handler
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "Success",
- handler: muxSuccess(),
+ desc: "Success",
+ builder: mockBuilder().
+ Route("POST /1.0/token",
+ servermock.ResponseFromFixture("token.json")).
+ Route("GET /1.0/domains", getDomainHandler()).
+ Route("GET /1.0/records",
+ servermock.ResponseFromFixture("records.json"),
+ servermock.CheckQueryParameter().With("domain_id", "1")).
+ Route("DELETE /1.0/records/3",
+ servermock.ResponseFromFixture("record_delete.json")),
},
{
- desc: "FailToFindZone",
- handler: muxFailToFindZone(),
+ desc: "FailToFindZone",
+ builder: mockBuilder().
+ Route("POST /1.0/token",
+ servermock.ResponseFromFixture("token.json")).
+ Route("GET /1.0/domains",
+ servermock.Noop().
+ WithStatusCode(http.StatusNotFound)),
expectedError: "vegadns: can't find Authoritative Zone for _acme-challenge.example.com. in CleanUp: Unable to find auth zone for fqdn _acme-challenge.example.com",
},
{
- desc: "FailToGetRecordID",
- handler: muxFailToGetRecordID(),
+ desc: "FailToGetRecordID",
+ builder: mockBuilder().
+ Route("POST /1.0/token",
+ servermock.ResponseFromFixture("token.json")).
+ Route("GET /1.0/domains", getDomainHandler()).
+ Route("GET /1.0/records",
+ servermock.Noop().
+ WithStatusCode(http.StatusNotFound),
+ servermock.CheckQueryParameter().With("domain_id", "1")),
expectedError: "vegadns: couldn't get Record ID in CleanUp: Got bad answer from VegaDNS on GetRecordID. Code: 404. Message: ",
},
}
@@ -107,12 +140,9 @@ func TestDNSProvider_CleanUp(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
- setupTest(t, test.handler)
+ provider := test.builder.Build(t)
- provider, err := NewDNSProvider()
- require.NoError(t, err)
-
- err = provider.CleanUp(testDomain, "token", "keyAuth")
+ err := provider.CleanUp(testDomain, "token", "keyAuth")
if test.expectedError == "" {
assert.NoError(t, err)
} else {
@@ -122,163 +152,36 @@ func TestDNSProvider_CleanUp(t *testing.T) {
}
}
-func muxSuccess() *http.ServeMux {
- mux := http.NewServeMux()
-
- mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, tokenResponseMock)
+func getDomainHandler() http.HandlerFunc {
+ return func(rw http.ResponseWriter, req *http.Request) {
+ if req.URL.Query().Get("search") == testDomain {
+ fmt.Fprint(rw, `
+{
+ "domains":[
+ {
+ "domain_id":1,
+ "domain":"example.com",
+ "status":"active",
+ "owner_id":0
+ }
+ ]
+}
+`)
return
}
- w.WriteHeader(http.StatusBadRequest)
- })
- mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Query().Get("search") == "example.com" {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, domainsResponseMock)
- return
- }
- w.WriteHeader(http.StatusNotFound)
- })
-
- mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case http.MethodGet:
- if r.URL.Query().Get("domain_id") == "1" {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, recordsResponseMock)
- return
- }
- w.WriteHeader(http.StatusNotFound)
- return
- case http.MethodPost:
- w.WriteHeader(http.StatusCreated)
- fmt.Fprint(w, recordCreatedResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/1.0/records/3", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodDelete {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, recordDeletedResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusNotFound)
- fmt.Printf("Not Found for Request: (%+v)\n\n", r)
- })
-
- return mux
+ rw.WriteHeader(http.StatusNotFound)
+ }
}
-func muxFailToFindZone() *http.ServeMux {
- mux := http.NewServeMux()
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {
+ envTest.Apply(map[string]string{
+ EnvKey: "key",
+ EnvSecret: "secret",
+ EnvURL: server.URL,
+ })
- mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, tokenResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusNotFound)
- })
-
- return mux
-}
-
-func muxFailToCreateTXT() *http.ServeMux {
- mux := http.NewServeMux()
-
- mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, tokenResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Query().Get("search") == testDomain {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, domainsResponseMock)
- return
- }
- w.WriteHeader(http.StatusNotFound)
- })
-
- mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case http.MethodGet:
- if r.URL.Query().Get("domain_id") == "1" {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, recordsResponseMock)
- return
- }
- w.WriteHeader(http.StatusNotFound)
- return
- case http.MethodPost:
- w.WriteHeader(http.StatusBadRequest)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- return mux
-}
-
-func muxFailToGetRecordID() *http.ServeMux {
- mux := http.NewServeMux()
-
- mux.HandleFunc("/1.0/token", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, tokenResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/1.0/domains", func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Query().Get("search") == testDomain {
- w.WriteHeader(http.StatusOK)
- fmt.Fprint(w, domainsResponseMock)
- return
- }
- w.WriteHeader(http.StatusNotFound)
- })
-
- mux.HandleFunc("/1.0/records", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodGet {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- return mux
-}
-
-func setupTest(t *testing.T, mux http.Handler) {
- t.Helper()
-
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- envTest.Apply(map[string]string{
- EnvKey: "key",
- EnvSecret: "secret",
- EnvURL: server.URL,
+ return NewDNSProvider()
})
}
diff --git a/providers/dns/vercel/internal/client_test.go b/providers/dns/vercel/internal/client_test.go
index 2a8b4eaea..eb5ee501d 100644
--- a/providers/dns/vercel/internal/client_test.go
+++ b/providers/dns/vercel/internal/client_test.go
@@ -1,71 +1,38 @@
package internal
import (
- "bytes"
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"), "123")
+ client.baseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := NewClient(OAuthStaticAccessToken(server.Client(), "secret"), "123")
- client.baseURL, _ = url.Parse(server.URL)
-
- return client, mux
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("Bearer secret"))
}
func TestClient_CreateRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v2/domains/example.com/records", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Bearer secret" {
- http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized)
- return
- }
-
- teamID := req.URL.Query().Get("teamId")
- if teamID != "123" {
- http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized)
- return
- }
-
- reqBody, err := io.ReadAll(req.Body)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- expectedReqBody := `{"name":"_acme-challenge.example.com.","type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":60}`
- assert.Equal(t, expectedReqBody, string(bytes.TrimSpace(reqBody)))
-
- rw.WriteHeader(http.StatusOK)
- _, err = fmt.Fprintf(rw, `{
+ client := mockBuilder().
+ Route("POST /v2/domains/example.com/records",
+ servermock.RawStringResponse(`{
"uid": "9e2eab60-0ba5-4dff-b481-2999c9764b84",
"updated": 1
- }`)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ }`),
+ servermock.CheckRequestJSONBody(`{"name":"_acme-challenge.example.com.","type":"TXT","value":"w6uP8Tcg6K2QR905Rms8iXTlksL6OD1KOWBxTK7wxPI","ttl":60}`),
+ servermock.CheckQueryParameter().Strict().
+ With("teamId", "123")).
+ Build(t)
record := Record{
Name: "_acme-challenge.example.com.",
@@ -86,27 +53,11 @@ func TestClient_CreateRecord(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client, mux := setupTest(t)
-
- mux.HandleFunc("/v2/domains/example.com/records/1234567", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
- return
- }
- auth := req.Header.Get("Authorization")
- if auth != "Bearer secret" {
- http.Error(rw, fmt.Sprintf("invalid API token: %s", auth), http.StatusUnauthorized)
- return
- }
-
- teamID := req.URL.Query().Get("teamId")
- if teamID != "123" {
- http.Error(rw, fmt.Sprintf("invalid team ID: %s", teamID), http.StatusUnauthorized)
- return
- }
-
- rw.WriteHeader(http.StatusOK)
- })
+ client := mockBuilder().
+ Route("DELETE /v2/domains/example.com/records/1234567", nil,
+ servermock.CheckQueryParameter().Strict().
+ With("teamId", "123")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com.", "1234567")
require.NoError(t, err)
diff --git a/providers/dns/versio/fixtures/error_failToCreateTXT.json b/providers/dns/versio/fixtures/error_failToCreateTXT.json
new file mode 100644
index 000000000..1e1784517
--- /dev/null
+++ b/providers/dns/versio/fixtures/error_failToCreateTXT.json
@@ -0,0 +1,6 @@
+{
+ "error": {
+ "code": 400,
+ "message": "ProcessError|DNS record invalid type _acme-challenge.example.eu. TST"
+ }
+}
diff --git a/providers/dns/versio/fixtures/error_failToFindZone.json b/providers/dns/versio/fixtures/error_failToFindZone.json
new file mode 100644
index 000000000..635b2bda1
--- /dev/null
+++ b/providers/dns/versio/fixtures/error_failToFindZone.json
@@ -0,0 +1,6 @@
+{
+ "error": {
+ "code": 401,
+ "message": "ObjectDoesNotExist|Domain not found"
+ }
+}
diff --git a/providers/dns/versio/fixtures/token.json b/providers/dns/versio/fixtures/token.json
new file mode 100644
index 000000000..0dc0dda25
--- /dev/null
+++ b/providers/dns/versio/fixtures/token.json
@@ -0,0 +1,5 @@
+{
+ "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f",
+ "token_type":"bearer",
+ "expires_in":3600
+}
diff --git a/providers/dns/versio/internal/client_test.go b/providers/dns/versio/internal/client_test.go
index 63b80ce4a..f3bf68c6d 100644
--- a/providers/dns/versio/internal/client_test.go
+++ b/providers/dns/versio/internal/client_test.go
@@ -1,61 +1,36 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern string, h http.HandlerFunc) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.BaseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, h)
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.BaseURL, _ = url.Parse(server.URL)
-
- return client
-}
-
-func writeFixture(rw http.ResponseWriter, filename string) {
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, _ = io.Copy(rw, file)
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("user", "secret"))
}
func TestClient_GetDomain(t *testing.T) {
- client := setupTest(t, "/domains/example.com", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Basic dXNlcjpzZWNyZXQ=" {
- http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized)
- return
- }
-
- writeFixture(rw, "get-domain.json")
- })
+ client := mockBuilder().
+ Route("GET /domains/example.com",
+ servermock.ResponseFromFixture("get-domain.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("show_dns_records", "true")).
+ Build(t)
records, err := client.GetDomain(t.Context(), "example.com")
require.NoError(t, err)
@@ -79,36 +54,22 @@ func TestClient_GetDomain(t *testing.T) {
}
func TestClient_GetDomain_error(t *testing.T) {
- client := setupTest(t, "/domains/example.com", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- rw.WriteHeader(http.StatusUnauthorized)
-
- writeFixture(rw, "get-domain-error.json")
- })
+ client := mockBuilder().
+ Route("GET /domains/example.com",
+ servermock.ResponseFromFixture("get-domain-error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
_, err := client.GetDomain(t.Context(), "example.com")
require.ErrorAs(t, err, &ErrorMessage{})
}
func TestClient_UpdateDomain(t *testing.T) {
- client := setupTest(t, "/domains/example.com/update", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- auth := req.Header.Get("Authorization")
- if auth != "Basic dXNlcjpzZWNyZXQ=" {
- http.Error(rw, "invalid credentials: "+auth, http.StatusUnauthorized)
- return
- }
-
- writeFixture(rw, "update-domain.json")
- })
+ client := mockBuilder().
+ Route("POST /domains/example.com/update",
+ servermock.ResponseFromFixture("update-domain.json"),
+ servermock.CheckRequestJSONBodyFromFile("update-domain-request.json")).
+ Build(t)
msg := &DomainInfo{DNSRecords: []Record{
{Type: "MX", Name: "example.com", Value: "fallback.axc.eu", Priority: 20, TTL: 3600},
@@ -147,16 +108,11 @@ func TestClient_UpdateDomain(t *testing.T) {
}
func TestClient_UpdateDomain_error(t *testing.T) {
- client := setupTest(t, "/domains/example.com/update", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- rw.WriteHeader(http.StatusUnauthorized)
-
- writeFixture(rw, "update-domain.json")
- })
+ client := mockBuilder().
+ Route("POST /domains/example.com/update",
+ servermock.ResponseFromFixture("update-domain-error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
msg := &DomainInfo{DNSRecords: []Record{
{Type: "MX", Name: "example.com", Value: "fallback.axc.eu", Priority: 20, TTL: 3600},
diff --git a/providers/dns/versio/internal/fixtures/update-domain-request.json b/providers/dns/versio/internal/fixtures/update-domain-request.json
new file mode 100644
index 000000000..f351678fc
--- /dev/null
+++ b/providers/dns/versio/internal/fixtures/update-domain-request.json
@@ -0,0 +1,78 @@
+{
+ "dns_records": [
+ {
+ "type": "MX",
+ "name": "example.com",
+ "value": "fallback.axc.eu",
+ "prio": 20,
+ "ttl": 3600
+ },
+ {
+ "type": "TXT",
+ "name": "example.com",
+ "value": "\"v=spf1 a mx ip4:127.0.0.1 a:spf.spamexperts.axc.nl ~all\"",
+ "ttl": 3600
+ },
+ {
+ "type": "A",
+ "name": "example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "A",
+ "name": "ftp.example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "A",
+ "name": "localhost.example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "A",
+ "name": "pop.example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "A",
+ "name": "smtp.example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "A",
+ "name": "www.example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "A",
+ "name": "dev.example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "A",
+ "name": "_domainkey.domain.com.example.com",
+ "value": "185.13.227.159",
+ "ttl": 14400
+ },
+ {
+ "type": "MX",
+ "name": "example.com",
+ "value": "spamfilter2.axc.eu",
+ "ttl": 3600
+ },
+ {
+ "type": "A",
+ "name": "redirect.example.com",
+ "value": "localhost",
+ "prio": 10,
+ "ttl": 14400
+ }
+ ]
+}
diff --git a/providers/dns/versio/versio_mock_test.go b/providers/dns/versio/versio_mock_test.go
deleted file mode 100644
index 07dc74e83..000000000
--- a/providers/dns/versio/versio_mock_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package versio
-
-const tokenResponseMock = `
-{
- "access_token":"699dd4ff-e381-46b8-8bf8-5de49dd56c1f",
- "token_type":"bearer",
- "expires_in":3600
-}
-`
-
-const tokenFailToFindZoneMock = `{"error":{"code":401,"message":"ObjectDoesNotExist|Domain not found"}}`
-
-const tokenFailToCreateTXTMock = `{"error":{"code":400,"message":"ProcessError|DNS record invalid type _acme-challenge.example.eu. TST"}}`
diff --git a/providers/dns/versio/versio_test.go b/providers/dns/versio/versio_test.go
index 09040ab4c..ea1ccc221 100644
--- a/providers/dns/versio/versio_test.go
+++ b/providers/dns/versio/versio_test.go
@@ -1,14 +1,12 @@
package versio
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"testing"
- "github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -125,21 +123,37 @@ func TestNewDNSProviderConfig(t *testing.T) {
func TestDNSProvider_Present(t *testing.T) {
testCases := []struct {
desc string
- handler http.Handler
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "Success",
- handler: muxSuccess(),
+ desc: "Success",
+ builder: mockBuilder().
+ Route("GET /domains/example.com",
+ servermock.ResponseFromFixture("token.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("show_dns_records", "true")).
+ Route("POST /domains/example.com/update",
+ servermock.ResponseFromFixture("token.json")),
},
{
- desc: "FailToFindZone",
- handler: muxFailToFindZone(),
+ desc: "FailToFindZone",
+ builder: mockBuilder().
+ Route("GET /domains/example.com",
+ servermock.ResponseFromFixture("error_failToFindZone.json").
+ WithStatusCode(http.StatusUnauthorized)),
expectedError: `versio: [status code: 401] 401: ObjectDoesNotExist|Domain not found`,
},
{
- desc: "FailToCreateTXT",
- handler: muxFailToCreateTXT(),
+ desc: "FailToCreateTXT",
+ builder: mockBuilder().
+ Route("GET /domains/example.com",
+ servermock.ResponseFromFixture("token.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("show_dns_records", "true")).
+ Route("POST /domains/example.com/update",
+ servermock.ResponseFromFixture("error_failToCreateTXT.json").
+ WithStatusCode(http.StatusBadRequest)),
expectedError: `versio: [status code: 400] 400: ProcessError|DNS record invalid type _acme-challenge.example.eu. TST`,
},
}
@@ -149,17 +163,9 @@ func TestDNSProvider_Present(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
- baseURL := setupTest(t, test.handler)
+ provider := test.builder.Build(t)
- envTest.Apply(map[string]string{
- EnvUsername: "me@example.com",
- EnvPassword: "secret",
- EnvEndpoint: baseURL,
- })
- provider, err := NewDNSProvider()
- require.NoError(t, err)
-
- err = provider.Present(testDomain, "token", "keyAuth")
+ err := provider.Present(testDomain, "token", "keyAuth")
if test.expectedError == "" {
require.NoError(t, err)
} else {
@@ -172,16 +178,25 @@ func TestDNSProvider_Present(t *testing.T) {
func TestDNSProvider_CleanUp(t *testing.T) {
testCases := []struct {
desc string
- handler http.Handler
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "Success",
- handler: muxSuccess(),
+ desc: "Success",
+ builder: mockBuilder().
+ Route("GET /domains/example.com",
+ servermock.ResponseFromFixture("token.json"),
+ servermock.CheckQueryParameter().Strict().
+ With("show_dns_records", "true")).
+ Route("POST /domains/example.com/update",
+ servermock.ResponseFromFixture("token.json")),
},
{
- desc: "FailToFindZone",
- handler: muxFailToFindZone(),
+ desc: "FailToFindZone",
+ builder: mockBuilder().
+ Route("GET /domains/example.com",
+ servermock.ResponseFromFixture("error_failToFindZone.json").
+ WithStatusCode(http.StatusUnauthorized)),
expectedError: `versio: [status code: 401] 401: ObjectDoesNotExist|Domain not found`,
},
}
@@ -191,18 +206,9 @@ func TestDNSProvider_CleanUp(t *testing.T) {
defer envTest.RestoreEnv()
envTest.ClearEnv()
- baseURL := setupTest(t, test.handler)
+ provider := test.builder.Build(t)
- envTest.Apply(map[string]string{
- EnvUsername: "me@example.com",
- EnvPassword: "secret",
- EnvEndpoint: baseURL,
- })
-
- provider, err := NewDNSProvider()
- require.NoError(t, err)
-
- err = provider.CleanUp(testDomain, "token", "keyAuth")
+ err := provider.CleanUp(testDomain, "token", "keyAuth")
if test.expectedError == "" {
require.NoError(t, err)
} else {
@@ -212,85 +218,6 @@ func TestDNSProvider_CleanUp(t *testing.T) {
}
}
-func muxSuccess() *http.ServeMux {
- mux := http.NewServeMux()
-
- mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodGet && r.URL.Query().Get("show_dns_records") == "true" {
- fmt.Fprint(w, tokenResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/domains/example.com/update", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- fmt.Fprint(w, tokenResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- log.Printf("unexpected request: %+v\n\n", r)
- data, _ := io.ReadAll(r.Body)
- defer func() { _ = r.Body.Close() }()
- log.Println(string(data))
- http.NotFound(w, r)
- })
-
- return mux
-}
-
-func muxFailToFindZone() *http.ServeMux {
- mux := http.NewServeMux()
-
- mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, _ *http.Request) {
- http.Error(w, tokenFailToFindZoneMock, http.StatusUnauthorized)
- })
-
- return mux
-}
-
-func muxFailToCreateTXT() *http.ServeMux {
- mux := http.NewServeMux()
-
- mux.HandleFunc("/domains/example.com", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodGet && r.URL.Query().Get("show_dns_records") == "true" {
- fmt.Fprint(w, tokenResponseMock)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/domains/example.com/update", func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- http.Error(w, tokenFailToCreateTXTMock, http.StatusBadRequest)
- return
- }
- w.WriteHeader(http.StatusBadRequest)
- })
-
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- log.Printf("unexpected request: %+v\n\n", r)
- data, _ := io.ReadAll(r.Body)
- defer func() { _ = r.Body.Close() }()
- log.Println(string(data))
- http.NotFound(w, r)
- })
-
- return mux
-}
-
-func setupTest(t *testing.T, handler http.Handler) string {
- t.Helper()
-
- server := httptest.NewServer(handler)
- t.Cleanup(server.Close)
-
- return server.URL
-}
-
func TestLivePresent(t *testing.T) {
if !envTest.IsLiveTest() {
t.Skip("skipping live test")
@@ -316,3 +243,15 @@ func TestLiveCleanUp(t *testing.T) {
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}
+
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {
+ envTest.Apply(map[string]string{
+ EnvUsername: "me@example.com",
+ EnvPassword: "secret",
+ EnvEndpoint: server.URL,
+ })
+
+ return NewDNSProvider()
+ })
+}
diff --git a/providers/dns/vinyldns/mock_test.go b/providers/dns/vinyldns/mock_test.go
deleted file mode 100644
index 54fd8e214..000000000
--- a/providers/dns/vinyldns/mock_test.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package vinyldns
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "os"
- "sync"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func setupTest(t *testing.T) (*http.ServeMux, *DNSProvider) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- config := NewDefaultConfig()
- config.AccessKey = "foo"
- config.SecretKey = "bar"
- config.Host = server.URL
-
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- return mux, p
-}
-
-type mockRouter struct {
- debug bool
-
- mu sync.Mutex
- routes map[string]map[string]http.HandlerFunc
-}
-
-func newMockRouter() *mockRouter {
- routes := map[string]map[string]http.HandlerFunc{
- http.MethodGet: {},
- http.MethodPost: {},
- http.MethodPut: {},
- http.MethodDelete: {},
- }
-
- return &mockRouter{
- routes: routes,
- }
-}
-
-func (h *mockRouter) Debug() *mockRouter {
- h.debug = true
-
- return h
-}
-
-func (h *mockRouter) Get(path string, statusCode int, filename string) *mockRouter {
- h.add(http.MethodGet, path, statusCode, filename)
- return h
-}
-
-func (h *mockRouter) Post(path string, statusCode int, filename string) *mockRouter {
- h.add(http.MethodPost, path, statusCode, filename)
- return h
-}
-
-func (h *mockRouter) Put(path string, statusCode int, filename string) *mockRouter {
- h.add(http.MethodPut, path, statusCode, filename)
- return h
-}
-
-func (h *mockRouter) Delete(path string, statusCode int, filename string) *mockRouter {
- h.add(http.MethodDelete, path, statusCode, filename)
- return h
-}
-
-func (h *mockRouter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
- h.mu.Lock()
- defer h.mu.Unlock()
-
- if h.debug {
- fmt.Println(req)
- }
-
- rt := h.routes[req.Method]
- if rt == nil {
- http.NotFound(rw, req)
- return
- }
-
- hdl := rt[req.URL.Path]
- if hdl == nil {
- http.NotFound(rw, req)
- return
- }
-
- hdl(rw, req)
-}
-
-func (h *mockRouter) add(method, path string, statusCode int, filename string) {
- h.routes[method][path] = func(rw http.ResponseWriter, req *http.Request) {
- rw.WriteHeader(statusCode)
-
- data, err := os.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- rw.Header().Set("Content-Type", "application/json")
- _, _ = rw.Write(data)
- }
-}
diff --git a/providers/dns/vinyldns/vinyldns_test.go b/providers/dns/vinyldns/vinyldns_test.go
index 8bfb192c8..6f5b9b328 100644
--- a/providers/dns/vinyldns/vinyldns_test.go
+++ b/providers/dns/vinyldns/vinyldns_test.go
@@ -2,10 +2,12 @@ package vinyldns
import (
"net/http"
+ "net/http/httptest"
"testing"
"time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
@@ -154,63 +156,86 @@ func TestNewDNSProviderConfig(t *testing.T) {
}
}
+func mockBuilder() *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.AccessKey = "foo"
+ config.SecretKey = "bar"
+ config.Host = server.URL
+
+ return NewDNSProviderConfig(config)
+ })
+}
+
func TestDNSProvider_Present(t *testing.T) {
testCases := []struct {
desc string
keyAuth string
- handler http.Handler
+ builder *servermock.Builder[*DNSProvider]
}{
{
desc: "new record",
keyAuth: "123456d==",
- handler: newMockRouter().
- Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName").
- Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll-empty").
- Post("/zones/"+zoneID+"/recordsets", http.StatusAccepted, "recordSetUpdate-create").
- Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-create"),
+ builder: mockBuilder().
+ Route("GET /zones/name/"+targetRootDomain+".",
+ servermock.ResponseFromFixture("zoneByName.json")).
+ Route("GET /zones/"+zoneID+"/recordsets",
+ servermock.ResponseFromFixture("recordSetsListAll-empty.json")).
+ Route("POST /zones/"+zoneID+"/recordsets",
+ servermock.ResponseFromFixture("recordSetUpdate-create.json").
+ WithStatusCode(http.StatusAccepted)).
+ Route("GET /zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID,
+ servermock.ResponseFromFixture("recordSetChange-create.json")),
},
{
desc: "existing record",
keyAuth: "123456d==",
- handler: newMockRouter().
- Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName").
- Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll"),
+ builder: mockBuilder().
+ Route("GET /zones/name/"+targetRootDomain+".",
+ servermock.ResponseFromFixture("zoneByName.json")).
+ Route("GET /zones/"+zoneID+"/recordsets",
+ servermock.ResponseFromFixture("recordSetsListAll.json")),
},
{
desc: "duplicate key",
keyAuth: "abc123!!",
- handler: newMockRouter().
- Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName").
- Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll").
- Put("/zones/"+zoneID+"/recordsets/"+recordID, http.StatusAccepted, "recordSetUpdate-create").
- Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-create"),
+ builder: mockBuilder().
+ Route("GET /zones/name/"+targetRootDomain+".",
+ servermock.ResponseFromFixture("zoneByName.json")).
+ Route("GET /zones/"+zoneID+"/recordsets",
+ servermock.ResponseFromFixture("recordSetsListAll.json")).
+ Route("PUT /zones/"+zoneID+"/recordsets/"+recordID,
+ servermock.ResponseFromFixture("recordSetUpdate-create.json").
+ WithStatusCode(http.StatusAccepted)).
+ Route("GET /zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID,
+ servermock.ResponseFromFixture("recordSetChange-create.json")),
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
+ provider := test.builder.Build(t)
- mux, p := setupTest(t)
- mux.Handle("/", test.handler)
-
- err := p.Present(targetDomain, "token"+test.keyAuth, test.keyAuth)
+ err := provider.Present(targetDomain, "token"+test.keyAuth, test.keyAuth)
require.NoError(t, err)
})
}
}
func TestDNSProvider_CleanUp(t *testing.T) {
- mux, p := setupTest(t)
+ provider := mockBuilder().
+ Route("GET /zones/name/"+targetRootDomain+".",
+ servermock.ResponseFromFixture("zoneByName.json")).
+ Route("GET /zones/"+zoneID+"/recordsets",
+ servermock.ResponseFromFixture("recordSetsListAll.json")).
+ Route("DELETE /zones/"+zoneID+"/recordsets/"+recordID,
+ servermock.ResponseFromFixture("recordSetDelete.json").
+ WithStatusCode(http.StatusAccepted)).
+ Route("GET /zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID,
+ servermock.ResponseFromFixture("recordSetChange-delete.json")).
+ Build(t)
- mux.Handle("/", newMockRouter().
- Get("/zones/name/"+targetRootDomain+".", http.StatusOK, "zoneByName").
- Get("/zones/"+zoneID+"/recordsets", http.StatusOK, "recordSetsListAll").
- Delete("/zones/"+zoneID+"/recordsets/"+recordID, http.StatusAccepted, "recordSetDelete").
- Get("/zones/"+zoneID+"/recordsets/"+newRecordSetID+"/changes/"+newCreateChangeID, http.StatusOK, "recordSetChange-delete"),
- )
-
- err := p.CleanUp(targetDomain, "123456d==", "123456d==")
+ err := provider.CleanUp(targetDomain, "123456d==", "123456d==")
require.NoError(t, err)
}
diff --git a/providers/dns/vkcloud/vkcloud.go b/providers/dns/vkcloud/vkcloud.go
index e76e87137..2aea7838c 100644
--- a/providers/dns/vkcloud/vkcloud.go
+++ b/providers/dns/vkcloud/vkcloud.go
@@ -119,7 +119,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
}
// Present creates a TXT record to fulfill the dns-01 challenge.
-func (r *DNSProvider) Present(domain, _, keyAuth string) error {
+func (d *DNSProvider) Present(domain, _, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
@@ -129,7 +129,7 @@ func (r *DNSProvider) Present(domain, _, keyAuth string) error {
authZone = dns01.UnFqdn(authZone)
- zones, err := r.client.ListZones()
+ zones, err := d.client.ListZones()
if err != nil {
return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err)
}
@@ -150,7 +150,7 @@ func (r *DNSProvider) Present(domain, _, keyAuth string) error {
return fmt.Errorf("vkcloud: %w", err)
}
- err = r.upsertTXTRecord(zoneUUID, subDomain, info.Value)
+ err = d.upsertTXTRecord(zoneUUID, subDomain, info.Value)
if err != nil {
return fmt.Errorf("vkcloud: %w", err)
}
@@ -159,7 +159,7 @@ func (r *DNSProvider) Present(domain, _, keyAuth string) error {
}
// CleanUp removes the TXT record matching the specified parameters.
-func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
+func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
@@ -169,7 +169,7 @@ func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
authZone = dns01.UnFqdn(authZone)
- zones, err := r.client.ListZones()
+ zones, err := d.client.ListZones()
if err != nil {
return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err)
}
@@ -191,7 +191,7 @@ func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
return fmt.Errorf("vkcloud: %w", err)
}
- err = r.removeTXTRecord(zoneUUID, subDomain, info.Value)
+ err = d.removeTXTRecord(zoneUUID, subDomain, info.Value)
if err != nil {
return fmt.Errorf("vkcloud: %w", err)
}
@@ -201,12 +201,12 @@ func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
-func (r *DNSProvider) Timeout() (timeout, interval time.Duration) {
- return r.config.PropagationTimeout, r.config.PollingInterval
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
}
-func (r *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error {
- records, err := r.client.ListTXTRecords(zoneUUID)
+func (d *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error {
+ records, err := d.client.ListTXTRecords(zoneUUID)
if err != nil {
return err
}
@@ -218,15 +218,15 @@ func (r *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error {
}
}
- return r.client.CreateTXTRecord(zoneUUID, &internal.DNSTXTRecord{
+ return d.client.CreateTXTRecord(zoneUUID, &internal.DNSTXTRecord{
Name: name,
Content: value,
- TTL: r.config.TTL,
+ TTL: d.config.TTL,
})
}
-func (r *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error {
- records, err := r.client.ListTXTRecords(zoneUUID)
+func (d *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error {
+ records, err := d.client.ListTXTRecords(zoneUUID)
if err != nil {
return err
}
@@ -234,7 +234,7 @@ func (r *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error {
name = dns01.UnFqdn(name)
for _, record := range records {
if record.Name == name && record.Content == value {
- return r.client.DeleteTXTRecord(zoneUUID, record.UUID)
+ return d.client.DeleteTXTRecord(zoneUUID, record.UUID)
}
}
diff --git a/providers/dns/vultr/vultr_test.go b/providers/dns/vultr/vultr_test.go
index aed891628..9be1a19b0 100644
--- a/providers/dns/vultr/vultr_test.go
+++ b/providers/dns/vultr/vultr_test.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vultr/govultr/v3"
@@ -159,53 +160,53 @@ func TestDNSProvider_getHostedZone(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- client := govultr.NewClient(nil)
- err := client.SetBaseURL(server.URL)
- require.NoError(t, err)
-
- p := &DNSProvider{client: client}
-
var pageCount int
- mux.HandleFunc("/v2/domains", func(rw http.ResponseWriter, req *http.Request) {
- pageCount++
+ provider := servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ client := govultr.NewClient(nil)
+ err := client.SetBaseURL(server.URL)
+ require.NoError(t, err)
- query := req.URL.Query()
- cursor, _ := strconv.Atoi(query.Get("cursor"))
- perPage, _ := strconv.Atoi(query.Get("per_page"))
+ return &DNSProvider{client: client}, nil
+ },
+ ).
+ Route("GET /v2/domains", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ pageCount++
- var next string
- if len(domains)/perPage > cursor {
- next = strconv.Itoa(cursor + 1)
- }
+ query := req.URL.Query()
+ cursor, _ := strconv.Atoi(query.Get("cursor"))
+ perPage, _ := strconv.Atoi(query.Get("per_page"))
- start := cursor * perPage
- if len(domains) < start {
- start = cursor * len(domains)
- }
+ var next string
+ if len(domains)/perPage > cursor {
+ next = strconv.Itoa(cursor + 1)
+ }
- end := min(len(domains), (cursor+1)*perPage)
+ start := cursor * perPage
+ if len(domains) < start {
+ start = cursor * len(domains)
+ }
- db := domainsBase{
- Domains: domains[start:end],
- Meta: &govultr.Meta{
- Total: len(domains),
- Links: &govultr.Links{Next: next},
- },
- }
+ end := min(len(domains), (cursor+1)*perPage)
- err = json.NewEncoder(rw).Encode(db)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
+ db := domainsBase{
+ Domains: domains[start:end],
+ Meta: &govultr.Meta{
+ Total: len(domains),
+ Links: &govultr.Links{Next: next},
+ },
+ }
- zone, err := p.getHostedZone(t.Context(), test.domain)
+ err := json.NewEncoder(rw).Encode(db)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ })).
+ Build(t)
+
+ zone, err := provider.getHostedZone(t.Context(), test.domain)
require.NoError(t, err)
assert.Equal(t, test.expected, zone)
diff --git a/providers/dns/webnames/internal/client_test.go b/providers/dns/webnames/internal/client_test.go
index ae14829a6..9507b6f98 100644
--- a/providers/dns/webnames/internal/client_test.go
+++ b/providers/dns/webnames/internal/client_test.go
@@ -1,74 +1,25 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
- "net/url"
- "os"
- "path"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, filename string, expectedParams url.Values) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("secret")
+ client.baseURL = server.URL
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
- http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
- return
- }
-
- err := req.ParseForm()
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- for k, v := range expectedParams {
- val := req.PostForm.Get(k)
- if len(v) == 0 {
- http.Error(rw, fmt.Sprintf("%s: no value", k), http.StatusBadRequest)
- return
- }
-
- if val != v[0] {
- http.Error(rw, fmt.Sprintf("%s: invalid value: %s != %s", k, val, v[0]), http.StatusBadRequest)
- return
- }
- }
-
- file, err := os.Open(path.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- defer func() { _ = file.Close() }()
-
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- server := httptest.NewServer(mux)
-
- client := NewClient("secret")
- client.baseURL = server.URL
- client.HTTPClient = server.Client()
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ )
}
func TestClient_AddTXTRecord(t *testing.T) {
@@ -93,13 +44,17 @@ func TestClient_AddTXTRecord(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- data := url.Values{}
- data.Set("domain", "example.com")
- data.Set("type", "TXT")
- data.Set("record", "foo:txtTXTtxt")
- data.Set("action", "add")
-
- client := setupTest(t, test.filename, data)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.ResponseFromFixture(test.filename),
+ servermock.CheckForm().Strict().
+ With("domain", "example.com").
+ With("type", "TXT").
+ With("record", "foo:txtTXTtxt").
+ With("action", "add").
+ With("apikey", "secret"),
+ ).
+ Build(t)
domain := "example.com"
subDomain := "foo"
@@ -133,13 +88,17 @@ func TestClient_RemoveTxtRecord(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
- data := url.Values{}
- data.Set("domain", "example.com")
- data.Set("type", "TXT")
- data.Set("record", "foo:txtTXTtxt")
- data.Set("action", "delete")
-
- client := setupTest(t, test.filename, data)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.ResponseFromFixture(test.filename),
+ servermock.CheckForm().Strict().
+ With("domain", "example.com").
+ With("type", "TXT").
+ With("record", "foo:txtTXTtxt").
+ With("action", "delete").
+ With("apikey", "secret"),
+ ).
+ Build(t)
domain := "example.com"
subDomain := "foo"
diff --git a/providers/dns/wedos/internal/client_test.go b/providers/dns/wedos/internal/client_test.go
index 4e011816b..f2515618a 100644
--- a/providers/dns/wedos/internal/client_test.go
+++ b/providers/dns/wedos/internal/client_test.go
@@ -4,58 +4,33 @@ import (
"fmt"
"net/http"
"net/http/httptest"
- "os"
"regexp"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupNew(t *testing.T, expectedForm, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.baseURL = server.URL
+ client.HTTPClient = server.Client()
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
- err := req.ParseForm()
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- exp := regexp.MustCompile(`"auth":"\w+",`)
-
- form := req.PostForm.Get("request")
- form = exp.ReplaceAllString(form, `"auth":"xxx",`)
-
- if form != expectedForm {
- t.Logf("invalid form data: %s", req.PostForm.Get("request"))
- http.Error(rw, fmt.Sprintf("invalid form data: %s", req.PostForm.Get("request")), http.StatusBadRequest)
- return
- }
-
- data, err := os.ReadFile(fmt.Sprintf("./fixtures/%s.json", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- rw.Header().Set("Content-Type", "application/json")
- _, _ = rw.Write(data)
- })
-
- client := NewClient("user", "secret")
- client.baseURL = server.URL
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded())
}
func TestClient_GetRecords(t *testing.T) {
- expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-rows-list","data":{"domain":"example.com"}}}`
- client := setupNew(t, expectedForm, commandDNSRowsList)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.ResponseFromFixture(commandDNSRowsList+".json"),
+ checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-rows-list","data":{"domain":"example.com"}}}`)).
+ Build(t)
records, err := client.GetRecords(t.Context(), "example.com.")
require.NoError(t, err)
@@ -94,9 +69,11 @@ func TestClient_GetRecords(t *testing.T) {
}
func TestClient_AddRecord(t *testing.T) {
- expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-add","data":{"domain":"example.com","name":"foo","ttl":1800,"type":"TXT","rdata":"foobar"}}}`
-
- client := setupNew(t, expectedForm, commandDNSRowAdd)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.ResponseFromFixture(commandDNSRowAdd+".json"),
+ checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-row-add","data":{"domain":"example.com","name":"foo","ttl":1800,"type":"TXT","rdata":"foobar"}}}`)).
+ Build(t)
record := DNSRow{
ID: "",
@@ -111,9 +88,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_update(t *testing.T) {
- expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-update","data":{"row_id":"1","domain":"example.com","ttl":1800,"type":"TXT","rdata":"foobar"}}}`
-
- client := setupNew(t, expectedForm, commandDNSRowUpdate)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.ResponseFromFixture(commandDNSRowUpdate+".json"),
+ checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-row-update","data":{"row_id":"1","domain":"example.com","ttl":1800,"type":"TXT","rdata":"foobar"}}}`)).
+ Build(t)
record := DNSRow{
ID: "1",
@@ -128,19 +107,45 @@ func TestClient_AddRecord_update(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-row-delete","data":{"row_id":"1","domain":"example.com","rdata":""}}}`
-
- client := setupNew(t, expectedForm, commandDNSRowDelete)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.ResponseFromFixture(commandDNSRowDelete+".json"),
+ checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-row-delete","data":{"row_id":"1","domain":"example.com","rdata":""}}}`)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com.", "1")
require.NoError(t, err)
}
func TestClient_Commit(t *testing.T) {
- expectedForm := `{"request":{"user":"user","auth":"xxx","command":"dns-domain-commit","data":{"name":"example.com"}}}`
-
- client := setupNew(t, expectedForm, commandDNSDomainCommit)
+ client := mockBuilder().
+ Route("POST /",
+ servermock.ResponseFromFixture(commandDNSDomainCommit+".json"),
+ checkFormRequest(`{"request":{"user":"user","auth":"xxx","command":"dns-domain-commit","data":{"name":"example.com"}}}`)).
+ Build(t)
err := client.Commit(t.Context(), "example.com.")
require.NoError(t, err)
}
+
+func checkFormRequest(data string) servermock.LinkFunc {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ err := req.ParseForm()
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ form := regexp.MustCompile(`"auth":"\w+",`).
+ ReplaceAllString(req.PostForm.Get("request"), `"auth":"xxx",`)
+
+ if form != data {
+ http.Error(rw, fmt.Sprintf("invalid form data: %s", req.PostForm.Get("request")), http.StatusBadRequest)
+ return
+ }
+
+ next.ServeHTTP(rw, req)
+ })
+ }
+}
diff --git a/providers/dns/westcn/internal/client_test.go b/providers/dns/westcn/internal/client_test.go
index 6e21d7f61..f7bdac5c0 100644
--- a/providers/dns/westcn/internal/client_test.go
+++ b/providers/dns/westcn/internal/client_test.go
@@ -1,123 +1,53 @@
package internal
import (
- "fmt"
- "io"
- "net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
"time"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/encoding/simplifiedchinese"
)
-type formExpectation func(values url.Values) error
-
-func setupTest(t *testing.T, filename string, expectations ...formExpectation) *Client {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc("POST /", func(rw http.ResponseWriter, req *http.Request) {
- err := req.ParseForm()
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- commons := []formExpectation{
- expectValue("username", "user"),
- expectNotEmpty("time"),
- expectNotEmpty("token"),
- }
-
- for _, common := range commons {
- err = common(req.Form)
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("user", "secret")
if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
+ return nil, err
}
- }
- for _, expectation := range expectations {
- err = expectation(req.Form)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
- }
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- rw.Header().Set("Content-Type", "application/json; Charset=gb2312")
-
- file, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = file.Close() }()
-
- rw.WriteHeader(http.StatusOK)
- _, err = io.Copy(rw, file)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("user", "secret")
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
-}
-
-func expectValue(key, value string) formExpectation {
- return func(values url.Values) error {
- if values.Get(key) != value {
- return fmt.Errorf("expected %s, got %s", value, values.Get(key))
- }
-
- return nil
- }
-}
-
-func expectNotEmpty(key string) formExpectation {
- return func(values url.Values) error {
- if values.Get(key) == "" {
- return fmt.Errorf("%s missing", key)
- }
-
- return nil
- }
-}
-
-func noop() formExpectation {
- return func(_ url.Values) error {
- return nil
- }
+ return client, nil
+ },
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded())
}
func TestClientAddRecord(t *testing.T) {
- expectValue("act", "adddnsrecord")
-
- client := setupTest(t, "adddnsrecord.json",
- expectValue("act", "adddnsrecord"),
- expectValue("domain", "example.com"),
- expectValue("host", "@"),
- expectValue("type", "TXT"),
- expectValue("value", "txtTXTtxt"),
- expectValue("ttl", "60"),
- )
+ client := mockBuilder().
+ Route("POST /domain/",
+ servermock.ResponseFromFixture("adddnsrecord.json").
+ WithHeader("Content-Type", "application/json", "Charset=gb2312"),
+ servermock.CheckQueryParameter().Strict().
+ With("act", "adddnsrecord"),
+ servermock.CheckForm().UsePostForm().Strict().
+ With("domain", "example.com").
+ With("host", "@").
+ With("ttl", "60").
+ With("type", "TXT").
+ With("value", "txtTXTtxt").
+ // With("act", "adddnsrecord").
+ With("username", "user").
+ WithRegexp("time", `\d+`).
+ WithRegexp("token", `[a-z0-9]{32}`),
+ ).
+ Build(t)
record := Record{
Domain: "example.com",
@@ -134,7 +64,13 @@ func TestClientAddRecord(t *testing.T) {
}
func TestClientAddRecord_error(t *testing.T) {
- client := setupTest(t, "error.json", noop())
+ client := mockBuilder().
+ Route("POST /domain/",
+ servermock.ResponseFromFixture("error.json").
+ WithHeader("Content-Type", "application/json", "Charset=gb2312"),
+ servermock.CheckQueryParameter().Strict().
+ With("act", "adddnsrecord")).
+ Build(t)
record := Record{
Domain: "example.com",
@@ -151,18 +87,34 @@ func TestClientAddRecord_error(t *testing.T) {
}
func TestClientDeleteRecord(t *testing.T) {
- client := setupTest(t, "deldnsrecord.json",
- expectValue("act", "deldnsrecord"),
- expectValue("domain", "example.com"),
- )
+ client := mockBuilder().
+ Route("POST /domain/",
+ servermock.ResponseFromFixture("deldnsrecord.json").
+ WithHeader("Content-Type", "application/json", "Charset=gb2312"),
+ servermock.CheckQueryParameter().Strict().
+ With("act", "deldnsrecord"),
+ servermock.CheckForm().UsePostForm().Strict().
+ With("id", "123").
+ With("domain", "example.com").
+ With("username", "user").
+ WithRegexp("time", `\d+`).
+ WithRegexp("token", `[a-z0-9]{32}`),
+ ).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", 123)
require.NoError(t, err)
}
func TestClientDeleteRecord_error(t *testing.T) {
- client := setupTest(t, "error.json", noop())
-
+ client := mockBuilder().
+ Route("POST /domain/",
+ servermock.ResponseFromFixture("error.json").
+ WithHeader("Content-Type", "application/json", "Charset=gb2312"),
+ servermock.CheckQueryParameter().Strict().
+ With("act", "deldnsrecord"),
+ ).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", 123)
require.Error(t, err)
diff --git a/providers/dns/yandex/internal/client_test.go b/providers/dns/yandex/internal/client_test.go
index 55de81bc7..4bb3357a6 100644
--- a/providers/dns/yandex/internal/client_test.go
+++ b/providers/dns/yandex/internal/client_test.go
@@ -1,327 +1,133 @@
package internal
import (
- "encoding/json"
- "net/http"
"net/http/httptest"
"net/url"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T) (*Client, *http.ServeMux) {
- t.Helper()
-
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
+func setupClient(server *httptest.Server) (*Client, error) {
client, err := NewClient("lego")
- require.NoError(t, err)
+ if err != nil {
+ return nil, err
+ }
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
- return client, mux
+ return client, nil
}
func TestAddRecord(t *testing.T) {
- testCases := []struct {
- desc string
- handler http.HandlerFunc
- data Record
- expectError bool
- }{
- {
- desc: "success",
- handler: func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodPost, r.Method)
- assert.Equal(t, "lego", r.Header.Get(pddTokenHeader))
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /add",
+ servermock.ResponseFromFixture("add_record.json"),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ servermock.CheckForm().Strict().
+ With("domain", "example.com").
+ With("subdomain", "foo").
+ With("ttl", "300").
+ With("content", "txtTXTtxtTXTtxtTXT").
+ With("type", "TXT")).
+ Build(t)
- err := r.ParseForm()
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- assert.Equal(t, `content=txtTXTtxtTXTtxtTXT&domain=example.com&subdomain=foo&ttl=300&type=TXT`, r.PostForm.Encode())
-
- response := AddResponse{
- Domain: "example.com",
- Record: &Record{
- ID: 1,
- Type: "TXT",
- Domain: "example.com",
- SubDomain: "foo",
- FQDN: "foo.example.com.",
- Content: "txtTXTtxtTXTtxtTXT",
- TTL: 300,
- },
- BaseResponse: BaseResponse{
- Success: "ok",
- },
- }
-
- err = json.NewEncoder(w).Encode(response)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- },
- data: Record{
- Domain: "example.com",
- Type: "TXT",
- Content: "txtTXTtxtTXTtxtTXT",
- SubDomain: "foo",
- TTL: 300,
- },
- },
- {
- desc: "error",
- handler: func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodPost, r.Method)
- assert.Equal(t, "lego", r.Header.Get(pddTokenHeader))
-
- err := r.ParseForm()
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- assert.Equal(t, `content=txtTXTtxtTXTtxtTXT&domain=example.com&subdomain=foo&ttl=300&type=TXT`, r.PostForm.Encode())
-
- response := AddResponse{
- Domain: "example.com",
- BaseResponse: BaseResponse{
- Success: "error",
- Error: "bad things",
- },
- }
-
- err = json.NewEncoder(w).Encode(response)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- },
- data: Record{
- Domain: "example.com",
- Type: "TXT",
- Content: "txtTXTtxtTXTtxtTXT",
- SubDomain: "foo",
- TTL: 300,
- },
- expectError: true,
- },
+ data := Record{
+ Domain: "example.com",
+ Type: "TXT",
+ Content: "txtTXTtxtTXTtxtTXT",
+ SubDomain: "foo",
+ TTL: 300,
}
- for _, test := range testCases {
- t.Run(test.desc, func(t *testing.T) {
- client, mux := setupTest(t)
+ record, err := client.AddRecord(t.Context(), data)
+ require.NoError(t, err)
+ require.NotNil(t, record)
+}
- mux.HandleFunc("/add", test.handler)
+func TestAddRecord_error(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /add",
+ servermock.ResponseFromFixture("add_record_error.json"),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded()).
+ Build(t)
- record, err := client.AddRecord(t.Context(), test.data)
- if test.expectError {
- require.Error(t, err)
- require.Nil(t, record)
- } else {
- require.NoError(t, err)
- require.NotNil(t, record)
- }
- })
+ data := Record{
+ Domain: "example.com",
+ Type: "TXT",
+ Content: "txtTXTtxtTXTtxtTXT",
+ SubDomain: "foo",
+ TTL: 300,
}
+
+ _, err := client.AddRecord(t.Context(), data)
+ require.EqualError(t, err, "error during operation: error bad things")
}
func TestRemoveRecord(t *testing.T) {
- testCases := []struct {
- desc string
- handler http.HandlerFunc
- data Record
- expectError bool
- }{
- {
- desc: "success",
- handler: func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodPost, r.Method)
- assert.Equal(t, "lego", r.Header.Get(pddTokenHeader))
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /del",
+ servermock.ResponseFromFixture("remove_record.json"),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded(),
+ servermock.CheckForm().Strict().
+ With("domain", "example.com").
+ With("record_id", "6")).
+ Build(t)
- err := r.ParseForm()
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- assert.Equal(t, `domain=example.com&record_id=6`, r.PostForm.Encode())
-
- response := RemoveResponse{
- Domain: "example.com",
- RecordID: 6,
- BaseResponse: BaseResponse{
- Success: "ok",
- },
- }
-
- err = json.NewEncoder(w).Encode(response)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- },
- data: Record{
- ID: 6,
- Domain: "example.com",
- },
- },
- {
- desc: "error",
- handler: func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodPost, r.Method)
- assert.Equal(t, "lego", r.Header.Get(pddTokenHeader))
-
- err := r.ParseForm()
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
-
- assert.Equal(t, `domain=example.com&record_id=6`, r.PostForm.Encode())
-
- response := RemoveResponse{
- Domain: "example.com",
- RecordID: 6,
- BaseResponse: BaseResponse{
- Success: "error",
- Error: "bad things",
- },
- }
-
- err = json.NewEncoder(w).Encode(response)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- },
- data: Record{
- ID: 6,
- Domain: "example.com",
- },
- expectError: true,
- },
+ data := Record{
+ ID: 6,
+ Domain: "example.com",
}
- for _, test := range testCases {
- t.Run(test.desc, func(t *testing.T) {
- client, mux := setupTest(t)
+ id, err := client.RemoveRecord(t.Context(), data)
+ require.NoError(t, err)
- mux.HandleFunc("/del", test.handler)
+ assert.Equal(t, 6, id)
+}
- id, err := client.RemoveRecord(t.Context(), test.data)
- if test.expectError {
- require.Error(t, err)
- require.Equal(t, 0, id)
- } else {
- require.NoError(t, err)
- require.Equal(t, 6, id)
- }
- })
+func TestRemoveRecord_error(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("POST /del",
+ servermock.ResponseFromFixture("remove_record_error.json"),
+ servermock.CheckHeader().
+ WithContentTypeFromURLEncoded()).
+ Build(t)
+
+ data := Record{
+ ID: 6,
+ Domain: "example.com",
}
+
+ _, err := client.RemoveRecord(t.Context(), data)
+ require.EqualError(t, err, "error during operation: error bad things")
}
func TestGetRecords(t *testing.T) {
- testCases := []struct {
- desc string
- handler http.HandlerFunc
- domain string
- expectError bool
- }{
- {
- desc: "success",
- handler: func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodGet, r.Method)
- assert.Equal(t, "lego", r.Header.Get(pddTokenHeader))
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /list",
+ servermock.ResponseFromFixture("get_records.json"),
+ servermock.CheckForm().Strict().
+ With("domain", "example.com")).
+ Build(t)
- assert.Equal(t, "domain=example.com", r.URL.RawQuery)
+ records, err := client.GetRecords(t.Context(), "example.com")
+ require.NoError(t, err)
- response := ListResponse{
- Domain: "example.com",
- Records: []Record{
- {
- ID: 1,
- Type: "TXT",
- Domain: "example.com",
- SubDomain: "foo",
- FQDN: "foo.example.com.",
- Content: "txtTXTtxtTXTtxtTXT",
- TTL: 300,
- },
- {
- ID: 2,
- Type: "NS",
- Domain: "example.com",
- SubDomain: "foo",
- FQDN: "foo.example.com.",
- Content: "bar",
- TTL: 300,
- },
- },
- BaseResponse: BaseResponse{
- Success: "ok",
- },
- }
-
- err := json.NewEncoder(w).Encode(response)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- },
- domain: "example.com",
- },
- {
- desc: "error",
- handler: func(w http.ResponseWriter, r *http.Request) {
- require.Equal(t, http.MethodGet, r.Method)
- assert.Equal(t, "lego", r.Header.Get(pddTokenHeader))
-
- assert.Equal(t, "domain=example.com", r.URL.RawQuery)
-
- response := ListResponse{
- Domain: "example.com",
- BaseResponse: BaseResponse{
- Success: "error",
- Error: "bad things",
- },
- }
-
- err := json.NewEncoder(w).Encode(response)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- },
- domain: "example.com",
- expectError: true,
- },
- }
-
- for _, test := range testCases {
- t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
- client, mux := setupTest(t)
-
- mux.HandleFunc("/list", test.handler)
-
- records, err := client.GetRecords(t.Context(), test.domain)
- if test.expectError {
- require.Error(t, err)
- require.Empty(t, records)
- } else {
- require.NoError(t, err)
- require.Len(t, records, 2)
- }
- })
- }
+ require.Len(t, records, 2)
+}
+
+func TestGetRecords_error(t *testing.T) {
+ client := servermock.NewBuilder[*Client](setupClient).
+ Route("GET /list",
+ servermock.ResponseFromFixture("get_records_error.json")).
+ Build(t)
+
+ _, err := client.GetRecords(t.Context(), "example.com")
+ require.EqualError(t, err, "error during operation: error bad things")
}
diff --git a/providers/dns/yandex/internal/fixtures/add_record.json b/providers/dns/yandex/internal/fixtures/add_record.json
new file mode 100644
index 000000000..1e4452d1d
--- /dev/null
+++ b/providers/dns/yandex/internal/fixtures/add_record.json
@@ -0,0 +1,13 @@
+{
+ "success": "ok",
+ "domain": "example.com",
+ "record": {
+ "record_id": 1,
+ "domain": "example.com",
+ "subdomain": "foo",
+ "fqdn": "foo.example.com.",
+ "ttl": 300,
+ "type": "TXT",
+ "content": "txtTXTtxtTXTtxtTXT"
+ }
+}
diff --git a/providers/dns/yandex/internal/fixtures/add_record_error.json b/providers/dns/yandex/internal/fixtures/add_record_error.json
new file mode 100644
index 000000000..932ccd674
--- /dev/null
+++ b/providers/dns/yandex/internal/fixtures/add_record_error.json
@@ -0,0 +1,5 @@
+{
+ "success": "error",
+ "error": "bad things",
+ "domain": "example.com"
+}
diff --git a/providers/dns/yandex/internal/fixtures/get_records.json b/providers/dns/yandex/internal/fixtures/get_records.json
new file mode 100644
index 000000000..e538834b4
--- /dev/null
+++ b/providers/dns/yandex/internal/fixtures/get_records.json
@@ -0,0 +1,24 @@
+{
+ "success": "ok",
+ "domain": "example.com",
+ "records": [
+ {
+ "record_id": 1,
+ "domain": "example.com",
+ "subdomain": "foo",
+ "fqdn": "foo.example.com.",
+ "ttl": 300,
+ "type": "TXT",
+ "content": "txtTXTtxtTXTtxtTXT"
+ },
+ {
+ "record_id": 2,
+ "domain": "example.com",
+ "subdomain": "foo",
+ "fqdn": "foo.example.com.",
+ "ttl": 300,
+ "type": "NS",
+ "content": "bar"
+ }
+ ]
+}
diff --git a/providers/dns/yandex/internal/fixtures/get_records_error.json b/providers/dns/yandex/internal/fixtures/get_records_error.json
new file mode 100644
index 000000000..932ccd674
--- /dev/null
+++ b/providers/dns/yandex/internal/fixtures/get_records_error.json
@@ -0,0 +1,5 @@
+{
+ "success": "error",
+ "error": "bad things",
+ "domain": "example.com"
+}
diff --git a/providers/dns/yandex/internal/fixtures/remove_record.json b/providers/dns/yandex/internal/fixtures/remove_record.json
new file mode 100644
index 000000000..3241ba9dc
--- /dev/null
+++ b/providers/dns/yandex/internal/fixtures/remove_record.json
@@ -0,0 +1,5 @@
+{
+ "success": "ok",
+ "domain": "example.com",
+ "record_id": 6
+}
diff --git a/providers/dns/yandex/internal/fixtures/remove_record_error.json b/providers/dns/yandex/internal/fixtures/remove_record_error.json
new file mode 100644
index 000000000..cd1471c9d
--- /dev/null
+++ b/providers/dns/yandex/internal/fixtures/remove_record_error.json
@@ -0,0 +1,6 @@
+{
+ "success": "error",
+ "error": "bad things",
+ "domain": "example.com",
+ "record_id": 6
+}
diff --git a/providers/dns/yandex360/internal/client_test.go b/providers/dns/yandex360/internal/client_test.go
index 83f66800f..aa21672e4 100644
--- a/providers/dns/yandex360/internal/client_test.go
+++ b/providers/dns/yandex360/internal/client_test.go
@@ -1,59 +1,39 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, pattern, method string, status int, filename string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client, err := NewClient("secret", 123456)
+ if err != nil {
+ return nil, err
+ }
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
+ client.HTTPClient = server.Client()
+ client.baseURL, _ = url.Parse(server.URL)
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", filename))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client, err := NewClient("secret", 123456)
- require.NoError(t, err)
-
- client.HTTPClient = server.Client()
- client.baseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithAuthorization("OAuth secret"))
}
func TestClient_AddRecord(t *testing.T) {
- client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns", http.MethodPost, http.StatusOK, "add-record.json")
+ client := mockBuilder().
+ Route("POST /directory/v1/org/123456/domains/example.com/dns",
+ servermock.ResponseFromFixture("add-record.json"),
+ servermock.CheckRequestJSONBody(`{"name":"_acme-challenge","text":"txtxtxt","ttl":60,"type":"TXT"}`)).
+ Build(t)
record := Record{
Name: "_acme-challenge",
@@ -77,7 +57,11 @@ func TestClient_AddRecord(t *testing.T) {
}
func TestClient_AddRecord_error(t *testing.T) {
- client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns", http.MethodGet, http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("POST /directory/v1/org/123456/domains/example.com/dns",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
record := Record{
Name: "_acme-challenge",
@@ -93,14 +77,21 @@ func TestClient_AddRecord_error(t *testing.T) {
}
func TestClient_DeleteRecord(t *testing.T) {
- client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns/789456", http.MethodDelete, http.StatusOK, "delete-record.json")
+ client := mockBuilder().
+ Route("DELETE /directory/v1/org/123456/domains/example.com/dns/789456",
+ servermock.ResponseFromFixture("delete-record.json")).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", 789456)
require.NoError(t, err)
}
func TestClient_DeleteRecord_error(t *testing.T) {
- client := setupTest(t, "/directory/v1/org/123456/domains/example.com/dns/789456", http.MethodDelete, http.StatusUnauthorized, "error.json")
+ client := mockBuilder().
+ Route("DELETE /directory/v1/org/123456/domains/example.com/dns/789456",
+ servermock.ResponseFromFixture("error.json").
+ WithStatusCode(http.StatusUnauthorized)).
+ Build(t)
err := client.DeleteRecord(t.Context(), "example.com", 789456)
require.Error(t, err)
diff --git a/providers/dns/yandexcloud/yandexcloud.go b/providers/dns/yandexcloud/yandexcloud.go
index 22da14404..ca44ab82b 100644
--- a/providers/dns/yandexcloud/yandexcloud.go
+++ b/providers/dns/yandexcloud/yandexcloud.go
@@ -103,7 +103,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
}
// Present creates a TXT record to fulfill the dns-01 challenge.
-func (r *DNSProvider) Present(domain, _, keyAuth string) error {
+func (d *DNSProvider) Present(domain, _, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
@@ -113,7 +113,7 @@ func (r *DNSProvider) Present(domain, _, keyAuth string) error {
ctx := context.Background()
- zones, err := r.getZones(ctx)
+ zones, err := d.getZones(ctx)
if err != nil {
return fmt.Errorf("yandexcloud: %w", err)
}
@@ -135,7 +135,7 @@ func (r *DNSProvider) Present(domain, _, keyAuth string) error {
return fmt.Errorf("yandexcloud: %w", err)
}
- err = r.upsertRecordSetData(ctx, zoneID, subDomain, info.Value)
+ err = d.upsertRecordSetData(ctx, zoneID, subDomain, info.Value)
if err != nil {
return fmt.Errorf("yandexcloud: %w", err)
}
@@ -144,7 +144,7 @@ func (r *DNSProvider) Present(domain, _, keyAuth string) error {
}
// CleanUp removes the TXT record matching the specified parameters.
-func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
+func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
@@ -154,7 +154,7 @@ func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
ctx := context.Background()
- zones, err := r.getZones(ctx)
+ zones, err := d.getZones(ctx)
if err != nil {
return fmt.Errorf("yandexcloud: %w", err)
}
@@ -176,7 +176,7 @@ func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
return fmt.Errorf("yandexcloud: %w", err)
}
- err = r.removeRecordSetData(ctx, zoneID, subDomain, info.Value)
+ err = d.removeRecordSetData(ctx, zoneID, subDomain, info.Value)
if err != nil {
return fmt.Errorf("yandexcloud: %w", err)
}
@@ -186,17 +186,17 @@ func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error {
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
-func (r *DNSProvider) Timeout() (timeout, interval time.Duration) {
- return r.config.PropagationTimeout, r.config.PollingInterval
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
}
// getZones retrieves available zones from yandex cloud.
-func (r *DNSProvider) getZones(ctx context.Context) ([]*ycdns.DnsZone, error) {
+func (d *DNSProvider) getZones(ctx context.Context) ([]*ycdns.DnsZone, error) {
list := &ycdns.ListDnsZonesRequest{
- FolderId: r.config.FolderID,
+ FolderId: d.config.FolderID,
}
- response, err := r.client.DNS().DnsZone().List(ctx, list)
+ response, err := d.client.DNS().DnsZone().List(ctx, list)
if err != nil {
return nil, errors.New("unable to fetch dns zones")
}
@@ -204,14 +204,14 @@ func (r *DNSProvider) getZones(ctx context.Context) ([]*ycdns.DnsZone, error) {
return response.GetDnsZones(), nil
}
-func (r *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, value string) error {
+func (d *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, value string) error {
get := &ycdns.GetDnsZoneRecordSetRequest{
DnsZoneId: zoneID,
Name: name,
Type: "TXT",
}
- exist, err := r.client.DNS().DnsZone().GetRecordSet(ctx, get)
+ exist, err := d.client.DNS().DnsZone().GetRecordSet(ctx, get)
if err != nil {
if !strings.Contains(err.Error(), "RecordSet not found") {
return err
@@ -221,7 +221,7 @@ func (r *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, val
record := &ycdns.RecordSet{
Name: name,
Type: "TXT",
- Ttl: int64(r.config.TTL),
+ Ttl: int64(d.config.TTL),
Data: []string{},
}
@@ -243,19 +243,19 @@ func (r *DNSProvider) upsertRecordSetData(ctx context.Context, zoneID, name, val
Additions: []*ycdns.RecordSet{record},
}
- _, err = r.client.DNS().DnsZone().UpdateRecordSets(ctx, update)
+ _, err = d.client.DNS().DnsZone().UpdateRecordSets(ctx, update)
return err
}
-func (r *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, value string) error {
+func (d *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, value string) error {
get := &ycdns.GetDnsZoneRecordSetRequest{
DnsZoneId: zoneID,
Name: name,
Type: "TXT",
}
- previousRecord, err := r.client.DNS().DnsZone().GetRecordSet(ctx, get)
+ previousRecord, err := d.client.DNS().DnsZone().GetRecordSet(ctx, get)
if err != nil {
if strings.Contains(err.Error(), "RecordSet not found") {
// RecordSet is not present, nothing to do
@@ -272,7 +272,7 @@ func (r *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, val
record := &ycdns.RecordSet{
Name: name,
Type: "TXT",
- Ttl: int64(r.config.TTL),
+ Ttl: int64(d.config.TTL),
Data: []string{},
}
@@ -291,7 +291,7 @@ func (r *DNSProvider) removeRecordSetData(ctx context.Context, zoneID, name, val
Additions: additions,
}
- _, err = r.client.DNS().DnsZone().UpdateRecordSets(ctx, update)
+ _, err = d.client.DNS().DnsZone().UpdateRecordSets(ctx, update)
return err
}
diff --git a/providers/dns/zoneee/internal/client_test.go b/providers/dns/zoneee/internal/client_test.go
index 04676877f..c2f0e781e 100644
--- a/providers/dns/zoneee/internal/client_test.go
+++ b/providers/dns/zoneee/internal/client_test.go
@@ -1,62 +1,34 @@
package internal
import (
- "fmt"
- "io"
"net/http"
"net/http/httptest"
"net/url"
- "os"
- "path/filepath"
"testing"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func setupTest(t *testing.T, method, pattern string, status int, file string) *Client {
- t.Helper()
+func mockBuilder() *servermock.Builder[*Client] {
+ return servermock.NewBuilder[*Client](
+ func(server *httptest.Server) (*Client, error) {
+ client := NewClient("user", "secret")
+ client.HTTPClient = server.Client()
+ client.BaseURL, _ = url.Parse(server.URL)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != method {
- http.Error(rw, fmt.Sprintf("unsupported method %s", req.Method), http.StatusBadRequest)
- return
- }
-
- if file == "" {
- rw.WriteHeader(status)
- return
- }
-
- open, err := os.Open(filepath.Join("fixtures", file))
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- defer func() { _ = open.Close() }()
-
- rw.WriteHeader(status)
- _, err = io.Copy(rw, open)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
- })
-
- client := NewClient("user", "secret")
- client.HTTPClient = server.Client()
- client.BaseURL, _ = url.Parse(server.URL)
-
- return client
+ return client, nil
+ },
+ servermock.CheckHeader().WithJSONHeaders().
+ WithBasicAuth("user", "secret"),
+ )
}
func TestClient_GetTxtRecords(t *testing.T) {
- client := setupTest(t, http.MethodGet, "/dns/example.com/txt", http.StatusOK, "get-txt-records.json")
+ client := mockBuilder().
+ Route("GET /dns/example.com/txt", servermock.ResponseFromFixture("get-txt-records.json")).
+ Build(t)
records, err := client.GetTxtRecords(t.Context(), "example.com")
require.NoError(t, err)
@@ -69,7 +41,12 @@ func TestClient_GetTxtRecords(t *testing.T) {
}
func TestClient_AddTxtRecord(t *testing.T) {
- client := setupTest(t, http.MethodPost, "/dns/example.com/txt", http.StatusCreated, "create-txt-record.json")
+ client := mockBuilder().
+ Route("POST /dns/example.com/txt",
+ servermock.ResponseFromFixture("create-txt-record.json").
+ WithStatusCode(http.StatusCreated),
+ servermock.CheckRequestJSONBody(`{"name":"prefix.example.com","destination":"server.example.com"}`)).
+ Build(t)
records, err := client.AddTxtRecord(t.Context(), "example.com", TXTRecord{Name: "prefix.example.com", Destination: "server.example.com"})
require.NoError(t, err)
@@ -82,7 +59,11 @@ func TestClient_AddTxtRecord(t *testing.T) {
}
func TestClient_RemoveTxtRecord(t *testing.T) {
- client := setupTest(t, http.MethodDelete, "/dns/example.com/txt/123", http.StatusNoContent, "")
+ client := mockBuilder().
+ Route("DELETE /dns/example.com/txt/123",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)).
+ Build(t)
err := client.RemoveTxtRecord(t.Context(), "example.com", "123")
require.NoError(t, err)
diff --git a/providers/dns/zoneee/zoneee_test.go b/providers/dns/zoneee/zoneee_test.go
index 1f2909fa7..6f50cf36e 100644
--- a/providers/dns/zoneee/zoneee_test.go
+++ b/providers/dns/zoneee/zoneee_test.go
@@ -6,17 +6,22 @@ import (
"net/http"
"net/http/httptest"
"net/url"
- "path"
"testing"
"time"
"github.com/go-acme/lego/v4/platform/tester"
+ "github.com/go-acme/lego/v4/platform/tester/servermock"
"github.com/go-acme/lego/v4/providers/dns/zoneee/internal"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
+const (
+ fakeUsername = "user"
+ fakeAPIKey = "secret"
+)
+
var envTest = tester.NewEnvTest(EnvEndpoint, EnvAPIUser, EnvAPIKey).
WithLiveTestRequirements(EnvAPIUser, EnvAPIKey).
WithDomain(envDomain)
@@ -94,7 +99,6 @@ func TestNewDNSProviderConfig(t *testing.T) {
desc string
apiUser string
apiKey string
- endpoint string
expected string
}{
{
@@ -124,10 +128,6 @@ func TestNewDNSProviderConfig(t *testing.T) {
config.APIKey = test.apiKey
config.Username = test.apiUser
- if test.endpoint != "" {
- config.Endpoint = mustParse(test.endpoint)
- }
-
p, err := NewDNSProviderConfig(config)
if test.expected == "" {
@@ -147,57 +147,33 @@ func TestDNSProvider_Present(t *testing.T) {
testCases := []struct {
desc string
- username string
- apiKey string
- handlers map[string]http.HandlerFunc
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "success",
- username: "bar",
- apiKey: "foo",
- handlers: map[string]http.HandlerFunc{
- path.Join("/", "dns", hostedZone, "txt"): mockHandlerCreateRecord,
- },
+ desc: "success",
+ builder: mockBuilder(fakeUsername, fakeAPIKey).
+ Route("POST /dns/"+hostedZone+"/txt",
+ mockHandlerCreateRecord()),
},
{
- desc: "invalid auth",
- username: "nope",
- apiKey: "foo",
- handlers: map[string]http.HandlerFunc{
- path.Join("/", "dns", hostedZone, "txt"): mockHandlerCreateRecord,
- },
+ desc: "invalid auth",
+ builder: mockBuilder("nope", "nope").
+ Route("POST /dns/"+hostedZone+"/txt", nil),
expectedError: "zoneee: unexpected status code: [status code: 401] body: Unauthorized",
},
{
desc: "error",
- username: "bar",
- apiKey: "foo",
+ builder: mockBuilder(fakeUsername, fakeAPIKey),
expectedError: "zoneee: unexpected status code: [status code: 404] body: 404 page not found",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
+ provider := test.builder.Build(t)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- for uri, handler := range test.handlers {
- mux.HandleFunc(uri, handler)
- }
-
- config := NewDefaultConfig()
- config.Endpoint = mustParse(server.URL)
- config.Username = test.username
- config.APIKey = test.apiKey
-
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- err = p.Present(domain, "token", "key")
+ err := provider.Present(domain, "token", "key")
if test.expectedError == "" {
require.NoError(t, err)
} else {
@@ -213,81 +189,49 @@ func TestDNSProvider_Cleanup(t *testing.T) {
testCases := []struct {
desc string
- username string
- apiKey string
- handlers map[string]http.HandlerFunc
+ builder *servermock.Builder[*DNSProvider]
expectedError string
}{
{
- desc: "success",
- username: "bar",
- apiKey: "foo",
- handlers: map[string]http.HandlerFunc{
- path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{{
- ID: "1234",
- Name: domain,
- Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM",
- Delete: true,
- Modify: true,
- }}),
- path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord,
- },
+ desc: "success",
+ builder: mockBuilder(fakeUsername, fakeAPIKey).
+ Route("GET /dns/"+hostedZone+"/txt",
+ mockHandlerGetRecords([]internal.TXTRecord{{
+ ID: "1234",
+ Name: domain,
+ Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM",
+ Delete: true,
+ Modify: true,
+ }})).
+ Route("DELETE /dns/"+hostedZone+"/txt/1234",
+ servermock.Noop().
+ WithStatusCode(http.StatusNoContent)),
},
{
- desc: "no txt records",
- username: "bar",
- apiKey: "foo",
- handlers: map[string]http.HandlerFunc{
- path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{}),
- path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord,
- },
+ desc: "no txt records",
+ builder: mockBuilder(fakeUsername, fakeAPIKey).
+ Route("GET /dns/"+hostedZone+"/txt",
+ mockHandlerGetRecords([]internal.TXTRecord{})),
expectedError: "zoneee: txt record does not exist for LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM",
},
{
- desc: "invalid auth",
- username: "nope",
- apiKey: "foo",
- handlers: map[string]http.HandlerFunc{
- path.Join("/", "dns", hostedZone, "txt"): mockHandlerGetRecords([]internal.TXTRecord{{
- ID: "1234",
- Name: domain,
- Destination: "LHDhK3oGRvkiefQnx7OOczTY5Tic_xZ6HcMOc_gmtoM",
- Delete: true,
- Modify: true,
- }}),
- path.Join("/", "dns", hostedZone, "txt", "1234"): mockHandlerDeleteRecord,
- },
+ desc: "invalid auth",
+ builder: mockBuilder("nope", "nope").
+ Route("GET /dns/"+hostedZone+"/txt", nil),
expectedError: "zoneee: unexpected status code: [status code: 401] body: Unauthorized",
},
{
desc: "error",
- username: "bar",
- apiKey: "foo",
+ builder: mockBuilder(fakeUsername, fakeAPIKey),
expectedError: "zoneee: unexpected status code: [status code: 404] body: 404 page not found",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- t.Parallel()
+ provider := test.builder.Build(t)
- mux := http.NewServeMux()
- server := httptest.NewServer(mux)
- t.Cleanup(server.Close)
-
- for uri, handler := range test.handlers {
- mux.HandleFunc(uri, handler)
- }
-
- config := NewDefaultConfig()
- config.Endpoint = mustParse(server.URL)
- config.Username = test.username
- config.APIKey = test.apiKey
-
- p, err := NewDNSProviderConfig(config)
- require.NoError(t, err)
-
- err = p.CleanUp(domain, "token", "key")
+ err := provider.CleanUp(domain, "token", "key")
if test.expectedError == "" {
require.NoError(t, err)
} else {
@@ -325,72 +269,57 @@ func TestLiveCleanUp(t *testing.T) {
require.NoError(t, err)
}
-func mustParse(rawURL string) *url.URL {
- uri, err := url.Parse(rawURL)
- if err != nil {
- panic(err)
- }
- return uri
+func mockBuilder(username, apiKey string) *servermock.Builder[*DNSProvider] {
+ return servermock.NewBuilder(
+ func(server *httptest.Server) (*DNSProvider, error) {
+ config := NewDefaultConfig()
+ config.Endpoint, _ = url.Parse(server.URL)
+ config.Username = username
+ config.APIKey = apiKey
+
+ return NewDNSProviderConfig(config)
+ },
+ checkBasicAuth())
}
-func mockHandlerCreateRecord(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodPost {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
+func mockHandlerCreateRecord() http.HandlerFunc {
+ return encodeJSONHandler(func(req *http.Request, rw http.ResponseWriter) (any, error) {
+ record := internal.TXTRecord{}
+ err := json.NewDecoder(req.Body).Decode(&record)
+ if err != nil {
+ return nil, err
+ }
- username, apiKey, ok := req.BasicAuth()
- if username != "bar" || apiKey != "foo" || !ok {
- rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key."))
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
+ record.ID = "1234"
+ record.Delete = true
+ record.Modify = true
+ record.ResourceURL = req.URL.String() + "/1234"
- record := internal.TXTRecord{}
- err := json.NewDecoder(req.Body).Decode(&record)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- record.ID = "1234"
- record.Delete = true
- record.Modify = true
- record.ResourceURL = req.URL.String() + "/1234"
-
- bytes, err := json.Marshal([]internal.TXTRecord{record})
- if err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
-
- if _, err = rw.Write(bytes); err != nil {
- http.Error(rw, err.Error(), http.StatusInternalServerError)
- return
- }
+ return []internal.TXTRecord{record}, nil
+ })
}
func mockHandlerGetRecords(records []internal.TXTRecord) http.HandlerFunc {
- return func(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodGet {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
-
- username, apiKey, ok := req.BasicAuth()
- if username != "bar" || apiKey != "foo" || !ok {
- rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key."))
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
-
- for _, value := range records {
- if value.ResourceURL == "" {
- value.ResourceURL = req.URL.String() + "/" + value.ID
+ return encodeJSONHandler(func(req *http.Request, rw http.ResponseWriter) (any, error) {
+ for _, record := range records {
+ if record.ResourceURL == "" {
+ record.ResourceURL = req.URL.String() + "/" + record.ID
}
}
- bytes, err := json.Marshal(records)
+ return records, nil
+ })
+}
+
+func encodeJSONHandler(build func(req *http.Request, rw http.ResponseWriter) (any, error)) http.HandlerFunc {
+ return func(rw http.ResponseWriter, req *http.Request) {
+ data, err := build(req, rw)
+ if err != nil {
+ http.Error(rw, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ bytes, err := json.Marshal(data)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@@ -403,18 +332,17 @@ func mockHandlerGetRecords(records []internal.TXTRecord) http.HandlerFunc {
}
}
-func mockHandlerDeleteRecord(rw http.ResponseWriter, req *http.Request) {
- if req.Method != http.MethodDelete {
- http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
- }
+func checkBasicAuth() servermock.LinkFunc {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ username, apiKey, ok := req.BasicAuth()
+ if username != fakeUsername || apiKey != fakeAPIKey || !ok {
+ rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key."))
+ http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+ return
+ }
- username, apiKey, ok := req.BasicAuth()
- if username != "bar" || apiKey != "foo" || !ok {
- rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "Please enter your username and API key."))
- http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
+ next.ServeHTTP(rw, req)
+ })
}
-
- rw.WriteHeader(http.StatusNoContent)
}