-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
869 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
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") | ||
} |
3 changes: 3 additions & 0 deletions
3
providers/dns/mittwald/internal/fixtures/dns-create-dns-zone.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
13
providers/dns/mittwald/internal/fixtures/dns-list-dns-zones.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": {} | ||
} | ||
} | ||
] |
Oops, something went wrong.