test: server client mock (#2571)

This commit is contained in:
Ludovic Fernandez 2025-07-12 13:57:15 +02:00 committed by GitHub
commit 52e167c930
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
275 changed files with 8834 additions and 10259 deletions

1
go.mod
View file

@ -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

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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)
})
}

View file

@ -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)

View file

@ -0,0 +1,7 @@
{
"fulldomain": "foo.example.com",
"subdomain": "foo",
"username": "user",
"password": "secret",
"server_url": "https://example.com"
}

View file

@ -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",

View file

@ -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
}
}
}

View file

@ -0,0 +1,7 @@
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
<Body>
<KasApi xmlns="https://kasserver.com/">
<Params>{"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}}</Params>
</KasApi>
</Body>
</Envelope>

View file

@ -0,0 +1,7 @@
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
<Body>
<KasApi xmlns="https://kasserver.com/">
<Params>{"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"delete_dns_settings","KasRequestParams":{"record_id":"57347450"}}</Params>
</KasApi>
</Body>
</Envelope>

View file

@ -0,0 +1,7 @@
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
<Body>
<KasApi xmlns="https://kasserver.com/">
<Params>{"kas_login":"user","kas_auth_type":"session","kas_auth_data":"593959ca04f0de9689b586c6a647d15d","kas_action":"get_dns_settings","KasRequestParams":{"zone_host":"example.com"}}</Params>
</KasApi>
</Body>
</Envelope>

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1,8 @@
{
"type": "txt",
"value": {
"text": "txtxtxt"
},
"name": "_acme-challenge",
"ttl": 600
}

View file

@ -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")

View file

@ -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{{}}

View file

@ -0,0 +1,11 @@
{
"adds": [
{
"name": "",
"ttl": 0,
"type": "",
"value": ""
}
],
"rems": null
}

View file

@ -0,0 +1,11 @@
{
"adds": null,
"rems": [
{
"name": "",
"ttl": 0,
"type": "",
"value": ""
}
]
}

View file

@ -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"}

View file

@ -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)
}
)
}

View file

@ -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)

View file

@ -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)

View file

@ -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.",

View file

@ -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."})

View file

@ -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)

View file

@ -0,0 +1,7 @@
{
"name": "test.com",
"value": "value",
"ttl": 300,
"priority": 0,
"type": "TXT"
}

View file

@ -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"
}
]

View file

@ -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)

View file

@ -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
}

View file

@ -0,0 +1,14 @@
{
"search": [
{
"name": "clientId",
"operator": "eq",
"value": "clientID"
},
{
"name": "domainName",
"operator": "eq",
"value": "example.com"
}
]
}

View file

@ -0,0 +1,8 @@
{
"items": [
{
"id": "A",
"domainName": "example.com"
}
]
}

View file

@ -0,0 +1,4 @@
{
"email": "email@example.com",
"password": "secret"
}

View file

@ -0,0 +1,5 @@
{
"auth": {
"accessToken": "at"
}
}

View file

@ -0,0 +1,3 @@
{
"soaTtl": 300
}

View file

@ -0,0 +1,6 @@
{
"domainId": "A",
"name": "_acme-challenge.example.com",
"value": "txt",
"type": "TXT"
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1 @@
{}

View file

@ -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",

View file

@ -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)

View file

@ -0,0 +1 @@
{}

View file

@ -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{

View file

@ -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)

View file

@ -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)

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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",

View file

@ -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)
})
}
}

View file

@ -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 - <ERRMSG>`.
If an error occurs the response body is `error - <ERRMSG>`.
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 - <ERRMSG>`.
If an error occurs the response body is `error - <ERRMSG>`.
Can be a POST or a GET.

View file

@ -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)

View file

@ -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"}

View file

@ -0,0 +1,8 @@
{
"id": 1,
"type": "TXT",
"name": "foo",
"value": "aaa",
"ttl": 60,
"sourceId": 123
}

View file

@ -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
}
]
}

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1,7 @@
{
"data": "txtTXTtxt",
"host": "example.com",
"id": 0,
"ttl": 300,
"type": "TXT"
}

View file

@ -0,0 +1,3 @@
{
"id": 1
}

View file

@ -0,0 +1,9 @@
[
{
"id": 1,
"host": "example.com",
"ttl": 3600,
"type": "TXT",
"data": "txtTXTtxt"
}
]

View file

@ -0,0 +1,9 @@
[
{
"id": 1,
"host": "example.com",
"ttl": 3600,
"type": "TXT",
"data": "txtTXTtxt"
}
]

View file

@ -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"
}
}
]

View file

@ -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")

View file

@ -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

View file

@ -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 == "" {

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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{})
}

View file

@ -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
}
}
}

View file

@ -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",

View file

@ -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

View file

@ -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)
}

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
<methodName>domain.zone.record.add</methodName>
<param>
<value>
<string>secret</string>
</value>
</param>
<param>
<value>
<int>1</int>
</value>
</param>
<param>
<value>
<int>123</int>
</value>
</param>
<param>
<value>
<struct>
<member>
<name>type</name>
<value>
<string>TXT</string>
</value>
</member>
<member>
<name>name</name>
<value>
<string>foo</string>
</value>
</member>
<member>
<name>value</name>
<value>
<string>content</string>
</value>
</member>
<member>
<name>ttl</name>
<value>
<int>120</int>
</value>
</member>
</struct>
</value>
</param>
</methodCall>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
<methodName>domain.zone.clone</methodName>
<param>
<value>
<string>secret</string>
</value>
</param>
<param>
<value>
<int>6</int>
</value>
</param>
<param>
<value>
<int>0</int>
</value>
</param>
<param>
<value>
<struct>
<member>
<name>name</name>
<value>
<string>foo</string>
</value>
</member>
</struct>
</value>
</param>
</methodCall>

View file

@ -0,0 +1,22 @@
<responseStruct>
<params>
<param>
<value>
<struct>
<member>
<name>id</name>
<value>
<int>1</int>
</value>
</member>
<member>
<name>foo</name>
<value>
<int>2</int>
</value>
</member>
</struct>
</value>
</param>
</params>
</responseStruct>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
<methodName>domain.zone.delete</methodName>
<param>
<value>
<string>secret</string>
</value>
</param>
<param>
<value>
<int>1</int>
</value>
</param>
</methodCall>

View file

@ -0,0 +1,9 @@
<responseBool>
<params>
<param>
<value>
<boolean>true</boolean>
</value>
</param>
</params>
</responseBool>

View file

@ -0,0 +1,2 @@
<responseStruct>
</responseStruct>

Some files were not shown because too many files have changed in this diff Show more