Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez committed Jun 4, 2024
1 parent 96bb0ba commit 1c4c129
Show file tree
Hide file tree
Showing 12 changed files with 869 additions and 0 deletions.
3 changes: 3 additions & 0 deletions providers/dns/dns_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/luadns"
"github.com/go-acme/lego/v4/providers/dns/mailinabox"
"github.com/go-acme/lego/v4/providers/dns/metaname"
"github.com/go-acme/lego/v4/providers/dns/mittwald"
"github.com/go-acme/lego/v4/providers/dns/mydnsjp"
"github.com/go-acme/lego/v4/providers/dns/mythicbeasts"
"github.com/go-acme/lego/v4/providers/dns/namecheap"
Expand Down Expand Up @@ -294,6 +295,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return dns01.NewDNSProviderManual()
case "metaname":
return metaname.NewDNSProvider()
case "mittwald":
return mittwald.NewDNSProvider()
case "mydnsjp":
return mydnsjp.NewDNSProvider()
case "mythicbeasts":
Expand Down
187 changes: 187 additions & 0 deletions providers/dns/mittwald/internal/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package internal

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"

"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
)

const defaultBaseURL = "https://api.mittwald.de/v2/"

const authorizationHeader = "Authorization"

// Client the Mittwald client.
type Client struct {
token string

baseURL *url.URL
HTTPClient *http.Client
}

// NewClient Creates a new Client.
func NewClient(token string) *Client {
baseURL, _ := url.Parse(defaultBaseURL)

return &Client{
token: token,
baseURL: baseURL,
HTTPClient: &http.Client{Timeout: 5 * time.Second},
}
}

// ListDomains List Domains.
// https://api.mittwald.de/v2/docs/#/Domain/domain-list-domains
func (c Client) ListDomains(ctx context.Context) ([]Domain, error) {
endpoint := c.baseURL.JoinPath("domains")

req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}

var result []Domain
err = c.do(req, &result)
if err != nil {
return nil, err
}

return result, nil
}

// ListDNSZones List DNSZones belonging to a Project.
// https://api.mittwald.de/v2/docs/#/Domain/dns-list-dns-zones
func (c Client) ListDNSZones(ctx context.Context, projectID string) ([]DNSZone, error) {
endpoint := c.baseURL.JoinPath("projects", projectID, "dns-zones")

req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}

var result []DNSZone
err = c.do(req, &result)
if err != nil {
return nil, err
}

return result, nil
}

// CreateDNSZone Create a DNSZone.
// https://api.mittwald.de/v2/docs/#/Domain/dns-create-dns-zone
func (c Client) CreateDNSZone(ctx context.Context, zone CreateDNSZoneRequest) (*DNSZone, error) {
endpoint := c.baseURL.JoinPath("dns-zones")

req, err := newJSONRequest(ctx, http.MethodPost, endpoint, zone)
if err != nil {
return nil, err
}

result := &DNSZone{}
err = c.do(req, result)
if err != nil {
return nil, err
}

return result, nil
}

// UpdateTXTRecord Update a record set on a DNSZone.
// https://api.mittwald.de/v2/docs/#/Domain/dns-update-record-set
func (c Client) UpdateTXTRecord(ctx context.Context, zoneID string, record TXTRecord) error {
endpoint := c.baseURL.JoinPath("dns-zones", zoneID, "record-sets", "txt")

req, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)
if err != nil {
return err
}

return c.do(req, nil)
}

// DeleteDNSZone Delete a DNSZone.
// https://api.mittwald.de/v2/docs/#/Domain/dns-delete-dns-zone
func (c Client) DeleteDNSZone(ctx context.Context, zoneID string) error {
endpoint := c.baseURL.JoinPath("dns-zones", zoneID)

req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}

return c.do(req, nil)
}

func (c Client) do(req *http.Request, result any) error {
req.Header.Set(authorizationHeader, "Bearer "+c.token)

resp, err := c.HTTPClient.Do(req)
if err != nil {
return errutils.NewHTTPDoError(req, err)
}

defer func() { _ = resp.Body.Close() }()

if resp.StatusCode/100 != 2 {
return parseError(req, resp)
}

if result == nil {
return nil
}

raw, err := io.ReadAll(resp.Body)
if err != nil {
return errutils.NewReadResponseError(req, resp.StatusCode, err)
}

err = json.Unmarshal(raw, result)
if err != nil {
return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
}

return nil
}

func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
buf := new(bytes.Buffer)

if payload != nil {
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return nil, fmt.Errorf("failed to create request JSON body: %w", err)
}
}

req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
}

req.Header.Set("Accept", "application/json")

if payload != nil {
req.Header.Set("Content-Type", "application/json")
}

return req, nil
}

func parseError(req *http.Request, resp *http.Response) error {
raw, _ := io.ReadAll(resp.Body)

var response APIError
err := json.Unmarshal(raw, &response)
if err != nil {
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
}

return fmt.Errorf("[status code %d] %w", resp.StatusCode, response)
}
157 changes: 157 additions & 0 deletions providers/dns/mittwald/internal/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package internal

import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func setupTest(t *testing.T, pattern string, handler http.HandlerFunc) *Client {
t.Helper()

mux := http.NewServeMux()
server := httptest.NewServer(mux)
t.Cleanup(server.Close)

mux.HandleFunc(pattern, handler)

client := NewClient("secret")
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)

return client
}

func testHandler(method string, statusCode int, filename string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
// fmt.Println(req) // FIXME

Check failure on line 36 in providers/dns/mittwald/internal/client_test.go

View workflow job for this annotation

GitHub Actions / Main Process

commentedOutCode: may want to remove commented-out code (gocritic)
if req.Method != method {
http.Error(rw, fmt.Sprintf(`{"message":"unsupported method: %s"}`, req.Method), http.StatusMethodNotAllowed)
return
}

auth := req.Header.Get(authorizationHeader)
if auth != "Bearer secret" {
http.Error(rw, fmt.Sprintf("invalid API Token: %s", auth), http.StatusUnauthorized)
return
}

rw.WriteHeader(statusCode)

if statusCode == http.StatusNoContent {
return
}

file, err := os.Open(filepath.Join("fixtures", filename))
if err != nil {
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
return
}

defer func() { _ = file.Close() }()

_, err = io.Copy(rw, file)
if err != nil {
http.Error(rw, fmt.Sprintf(`{"message":"%v"}`, err), http.StatusInternalServerError)
return
}
}
}

func TestClient_ListDomains(t *testing.T) {
client := setupTest(t, "/domains", testHandler(http.MethodGet, http.StatusOK, "domain-list-domains.json"))

domains, err := client.ListDomains(context.Background())
require.NoError(t, err)

require.Len(t, domains, 1)

expected := []Domain{{
Domain: "string",
DomainID: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
ProjectID: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}}

assert.Equal(t, expected, domains)
}

func TestClient_ListDomains_error(t *testing.T) {
client := setupTest(t, "/domains", testHandler(http.MethodGet, http.StatusBadRequest, "error-client.json"))

_, err := client.ListDomains(context.Background())
require.EqualError(t, err, "[status code 400] ValidationError: Validation failed [format: should be string (.address.street, email)]")
}

func TestClient_ListDNSZones(t *testing.T) {
client := setupTest(t, "/projects/my-project-id/dns-zones", testHandler(http.MethodGet, http.StatusOK, "dns-list-dns-zones.json"))

zones, err := client.ListDNSZones(context.Background(), "my-project-id")
require.NoError(t, err)

require.Len(t, zones, 1)

expected := []DNSZone{{
ID: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
Domain: "string",
RecordSet: &RecordSet{
TXT: &TXTRecord{},
},
}}

assert.Equal(t, expected, zones)
}

func TestClient_CreateDNSZone(t *testing.T) {
client := setupTest(t, "/dns-zones", testHandler(http.MethodPost, http.StatusCreated, "dns-create-dns-zone.json"))

request := CreateDNSZoneRequest{
Name: "test",
ParentZoneID: "my-parent-zone-id",
}

zone, err := client.CreateDNSZone(context.Background(), request)
require.NoError(t, err)

expected := &DNSZone{
ID: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}

assert.Equal(t, expected, zone)
}

func TestClient_UpdateTXTRecord(t *testing.T) {
client := setupTest(t, "/dns-zones/my-zone-id/record-sets/txt", testHandler(http.MethodPut, http.StatusNoContent, ""))

record := TXTRecord{
Settings: Settings{
TTL: TTL{Auto: true},
},
Entries: []string{"txt"},
}

err := client.UpdateTXTRecord(context.Background(), "my-zone-id", record)
require.NoError(t, err)
}

func TestClient_DeleteDNSZone(t *testing.T) {
client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodDelete, http.StatusOK, ""))

err := client.DeleteDNSZone(context.Background(), "my-zone-id")
require.NoError(t, err)
}

func TestClient_DeleteDNSZone_error(t *testing.T) {
client := setupTest(t, "/dns-zones/my-zone-id", testHandler(http.MethodDelete, http.StatusInternalServerError, "error.json"))

err := client.DeleteDNSZone(context.Background(), "my-zone-id")
assert.EqualError(t, err, "[status code 500] InternalServerError: Something went wrong")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
13 changes: 13 additions & 0 deletions providers/dns/mittwald/internal/fixtures/dns-list-dns-zones.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[
{
"domain": "string",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"recordSet": {
"cname": {},
"combinedARecords": {},
"mx": {},
"srv": {},
"txt": {}
}
}
]
Loading

0 comments on commit 1c4c129

Please sign in to comment.